From dd4187cf406ea05c8dfcac10c05e7142ee59e027 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 24 Apr 2026 10:05:38 -0400 Subject: [PATCH 1/4] fix: Correct request param placement for POST endpoints Parameter-group dispatch fields (passwords, resource IDs, role slugs) were incorrectly sent as query parameters on POST requests. They belong in the JSON request body. Adds ExtraBodyParams support to WorkOSRequest and merges them into the serialized JSON at request time. --- .../Client/Utilities/RequestUtilities.cs | 16 +++++++++ .../Client/_interfaces/WorkOSRequest.cs | 17 +++++++++ .../Authorization/AuthorizationService.cs | 36 +++++++++---------- .../UserManagement/UserManagementService.cs | 20 +++++------ 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/WorkOS.net/Client/Utilities/RequestUtilities.cs b/src/WorkOS.net/Client/Utilities/RequestUtilities.cs index 1ae3478f..22be4757 100644 --- a/src/WorkOS.net/Client/Utilities/RequestUtilities.cs +++ b/src/WorkOS.net/Client/Utilities/RequestUtilities.cs @@ -14,6 +14,7 @@ namespace WorkOS using System.Reflection; using System.Text; using Newtonsoft.Json; + using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; /// @@ -67,6 +68,21 @@ public static HttpContent CreateHttpContent(WorkOSRequest request) if (request.IsJsonContentType) { var jsonOptions = ToJsonString(options); + + // Merge extra body params (from parameter-group dispatch) into + // the serialized JSON so that fields like password / password_hash + // appear as top-level body keys instead of query parameters. + if (request.ExtraBodyParams != null && request.ExtraBodyParams.Count > 0) + { + var jobj = JObject.Parse(jsonOptions); + foreach (var kvp in request.ExtraBodyParams) + { + jobj[kvp.Key] = kvp.Value; + } + + jsonOptions = jobj.ToString(Formatting.None); + } + var content = new StringContent(jsonOptions, Encoding.UTF8); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return content; diff --git a/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs b/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs index e2d194c8..4a76346e 100644 --- a/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs +++ b/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs @@ -52,6 +52,13 @@ public class WorkOSRequest /// internal Dictionary? ExtraQueryParams { get; set; } + /// + /// Extra body parameters injected by parameter-group dispatch + /// (e.g. password variants for user creation). These are merged + /// into the JSON request body alongside the options-derived fields. + /// + internal Dictionary? ExtraBodyParams { get; set; } + /// /// Append an extra query parameter to the request. /// @@ -60,5 +67,15 @@ internal void AddQueryParam(string key, string value) this.ExtraQueryParams ??= new Dictionary(); this.ExtraQueryParams[key] = value; } + + /// + /// Append an extra body parameter to the request. The value is + /// merged into the serialized JSON body. + /// + internal void AddBodyParam(string key, string value) + { + this.ExtraBodyParams ??= new Dictionary(); + this.ExtraBodyParams[key] = value; + } } } diff --git a/src/WorkOS.net/Services/Authorization/AuthorizationService.cs b/src/WorkOS.net/Services/Authorization/AuthorizationService.cs index 4b8745c0..49738c0f 100644 --- a/src/WorkOS.net/Services/Authorization/AuthorizationService.cs +++ b/src/WorkOS.net/Services/Authorization/AuthorizationService.cs @@ -47,19 +47,19 @@ public virtual async Task CheckAsync(string organizationMemb { if (byId.ResourceId != null) { - request.AddQueryParam("resource_id", byId.ResourceId); + request.AddBodyParam("resource_id", byId.ResourceId); } } else if (options?.ResourceTarget is AuthorizationResourceTargetByExternalId byExternalId) { if (byExternalId.ResourceExternalId != null) { - request.AddQueryParam("resource_external_id", byExternalId.ResourceExternalId); + request.AddBodyParam("resource_external_id", byExternalId.ResourceExternalId); } if (byExternalId.ResourceTypeSlug != null) { - request.AddQueryParam("resource_type_slug", byExternalId.ResourceTypeSlug); + request.AddBodyParam("resource_type_slug", byExternalId.ResourceTypeSlug); } } @@ -254,19 +254,19 @@ public virtual async Task AssignRoleAsync(string organizationMem { if (byId.ResourceId != null) { - request.AddQueryParam("resource_id", byId.ResourceId); + request.AddBodyParam("resource_id", byId.ResourceId); } } else if (options?.ResourceTarget is AuthorizationResourceTargetByExternalId byExternalId) { if (byExternalId.ResourceExternalId != null) { - request.AddQueryParam("resource_external_id", byExternalId.ResourceExternalId); + request.AddBodyParam("resource_external_id", byExternalId.ResourceExternalId); } if (byExternalId.ResourceTypeSlug != null) { - request.AddQueryParam("resource_type_slug", byExternalId.ResourceTypeSlug); + request.AddBodyParam("resource_type_slug", byExternalId.ResourceTypeSlug); } } @@ -301,19 +301,19 @@ public virtual async Task RemoveRoleAsync(string organizationMembershipId, Autho { if (byId.ResourceId != null) { - request.AddQueryParam("resource_id", byId.ResourceId); + request.AddBodyParam("resource_id", byId.ResourceId); } } else if (options?.ResourceTarget is AuthorizationResourceTargetByExternalId byExternalId) { if (byExternalId.ResourceExternalId != null) { - request.AddQueryParam("resource_external_id", byExternalId.ResourceExternalId); + request.AddBodyParam("resource_external_id", byExternalId.ResourceExternalId); } if (byExternalId.ResourceTypeSlug != null) { - request.AddQueryParam("resource_type_slug", byExternalId.ResourceTypeSlug); + request.AddBodyParam("resource_type_slug", byExternalId.ResourceTypeSlug); } } @@ -552,19 +552,19 @@ public virtual async Task UpdateOrganizationResourceAsync { if (byId.ParentResourceId != null) { - request.AddQueryParam("parent_resource_id", byId.ParentResourceId); + request.AddBodyParam("parent_resource_id", byId.ParentResourceId); } } else if (options?.ParentResource is AuthorizationParentResourceByExternalId byExternalId) { if (byExternalId.ParentResourceExternalId != null) { - request.AddQueryParam("parent_resource_external_id", byExternalId.ParentResourceExternalId); + request.AddBodyParam("parent_resource_external_id", byExternalId.ParentResourceExternalId); } if (byExternalId.ParentResourceTypeSlug != null) { - request.AddQueryParam("parent_resource_type_slug", byExternalId.ParentResourceTypeSlug); + request.AddBodyParam("parent_resource_type_slug", byExternalId.ParentResourceTypeSlug); } } @@ -712,19 +712,19 @@ public virtual async Task CreateResourceAsync(Authorizati { if (byId.ParentResourceId != null) { - request.AddQueryParam("parent_resource_id", byId.ParentResourceId); + request.AddBodyParam("parent_resource_id", byId.ParentResourceId); } } else if (options?.ParentResource is AuthorizationParentResourceByExternalId byExternalId) { if (byExternalId.ParentResourceExternalId != null) { - request.AddQueryParam("parent_resource_external_id", byExternalId.ParentResourceExternalId); + request.AddBodyParam("parent_resource_external_id", byExternalId.ParentResourceExternalId); } if (byExternalId.ParentResourceTypeSlug != null) { - request.AddQueryParam("parent_resource_type_slug", byExternalId.ParentResourceTypeSlug); + request.AddBodyParam("parent_resource_type_slug", byExternalId.ParentResourceTypeSlug); } } @@ -779,19 +779,19 @@ public virtual async Task UpdateResourceAsync(string reso { if (byId.ParentResourceId != null) { - request.AddQueryParam("parent_resource_id", byId.ParentResourceId); + request.AddBodyParam("parent_resource_id", byId.ParentResourceId); } } else if (options?.ParentResource is AuthorizationParentResourceByExternalId byExternalId) { if (byExternalId.ParentResourceExternalId != null) { - request.AddQueryParam("parent_resource_external_id", byExternalId.ParentResourceExternalId); + request.AddBodyParam("parent_resource_external_id", byExternalId.ParentResourceExternalId); } if (byExternalId.ParentResourceTypeSlug != null) { - request.AddQueryParam("parent_resource_type_slug", byExternalId.ParentResourceTypeSlug); + request.AddBodyParam("parent_resource_type_slug", byExternalId.ParentResourceTypeSlug); } } diff --git a/src/WorkOS.net/Services/UserManagement/UserManagementService.cs b/src/WorkOS.net/Services/UserManagement/UserManagementService.cs index 0744b56e..c9fa1f5b 100644 --- a/src/WorkOS.net/Services/UserManagement/UserManagementService.cs +++ b/src/WorkOS.net/Services/UserManagement/UserManagementService.cs @@ -415,17 +415,17 @@ public virtual async Task CreateAsync(UserManagementCreateOptions options, { if (plaintext.Password != null) { - request.AddQueryParam("password", plaintext.Password); + request.AddBodyParam("password", plaintext.Password); } } else if (options?.Password is UserManagementPasswordHashed hashed) { if (hashed.PasswordHash != null) { - request.AddQueryParam("password_hash", hashed.PasswordHash); + request.AddBodyParam("password_hash", hashed.PasswordHash); } - request.AddQueryParam("password_hash_type", JsonConvert.SerializeObject(hashed.PasswordHashType).Trim('"')); + request.AddBodyParam("password_hash_type", JsonConvert.SerializeObject(hashed.PasswordHashType).Trim('"')); } return await this.Client.MakeAPIRequest(request, cancellationToken); @@ -498,17 +498,17 @@ public virtual async Task UpdateAsync(string id, UserManagementUpdateOptio { if (plaintext.Password != null) { - request.AddQueryParam("password", plaintext.Password); + request.AddBodyParam("password", plaintext.Password); } } else if (options?.Password is UserManagementPasswordHashed hashed) { if (hashed.PasswordHash != null) { - request.AddQueryParam("password_hash", hashed.PasswordHash); + request.AddBodyParam("password_hash", hashed.PasswordHash); } - request.AddQueryParam("password_hash_type", JsonConvert.SerializeObject(hashed.PasswordHashType).Trim('"')); + request.AddBodyParam("password_hash_type", JsonConvert.SerializeObject(hashed.PasswordHashType).Trim('"')); } return await this.Client.MakeAPIRequest(request, cancellationToken); @@ -920,14 +920,14 @@ public virtual async Task CreateOrganizationMembershipAs { if (single.RoleSlug != null) { - request.AddQueryParam("role_slug", single.RoleSlug); + request.AddBodyParam("role_slug", single.RoleSlug); } } else if (options?.Role is UserManagementRoleMultiple multiple) { if (multiple.RoleSlugs != null) { - request.AddQueryParam("role_slugs", string.Join(",", multiple.RoleSlugs)); + request.AddBodyParam("role_slugs", string.Join(",", multiple.RoleSlugs)); } } @@ -982,14 +982,14 @@ public virtual async Task UpdateOrganizationMembershipAs { if (single.RoleSlug != null) { - request.AddQueryParam("role_slug", single.RoleSlug); + request.AddBodyParam("role_slug", single.RoleSlug); } } else if (options?.Role is UserManagementRoleMultiple multiple) { if (multiple.RoleSlugs != null) { - request.AddQueryParam("role_slugs", string.Join(",", multiple.RoleSlugs)); + request.AddBodyParam("role_slugs", string.Join(",", multiple.RoleSlugs)); } } From fa0d742a9badeb13d967ba342ab14f1faec59ad3 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 24 Apr 2026 11:54:26 -0400 Subject: [PATCH 2/4] fix: Support non-string types in extra body params ExtraBodyParams was typed as Dictionary, so array fields like role_slugs were comma-joined into a single string ("admin,member") instead of serialized as a JSON array (["admin","member"]). Widening to Dictionary and serializing via JToken.FromObject lets callers pass arrays and other complex types that the API expects. --- src/WorkOS.net/Client/Utilities/RequestUtilities.cs | 2 +- src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs | 9 +++++---- .../Services/UserManagement/UserManagementService.cs | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/WorkOS.net/Client/Utilities/RequestUtilities.cs b/src/WorkOS.net/Client/Utilities/RequestUtilities.cs index 22be4757..fc17b598 100644 --- a/src/WorkOS.net/Client/Utilities/RequestUtilities.cs +++ b/src/WorkOS.net/Client/Utilities/RequestUtilities.cs @@ -77,7 +77,7 @@ public static HttpContent CreateHttpContent(WorkOSRequest request) var jobj = JObject.Parse(jsonOptions); foreach (var kvp in request.ExtraBodyParams) { - jobj[kvp.Key] = kvp.Value; + jobj[kvp.Key] = JToken.FromObject(kvp.Value, JsonSerializer.Create(JsonSettings)); } jsonOptions = jobj.ToString(Formatting.None); diff --git a/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs b/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs index 4a76346e..aeefd183 100644 --- a/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs +++ b/src/WorkOS.net/Client/_interfaces/WorkOSRequest.cs @@ -57,7 +57,7 @@ public class WorkOSRequest /// (e.g. password variants for user creation). These are merged /// into the JSON request body alongside the options-derived fields. /// - internal Dictionary? ExtraBodyParams { get; set; } + internal Dictionary? ExtraBodyParams { get; set; } /// /// Append an extra query parameter to the request. @@ -70,11 +70,12 @@ internal void AddQueryParam(string key, string value) /// /// Append an extra body parameter to the request. The value is - /// merged into the serialized JSON body. + /// merged into the serialized JSON body. Accepts strings, arrays, + /// or other objects that will be serialized as their native JSON type. /// - internal void AddBodyParam(string key, string value) + internal void AddBodyParam(string key, object value) { - this.ExtraBodyParams ??= new Dictionary(); + this.ExtraBodyParams ??= new Dictionary(); this.ExtraBodyParams[key] = value; } } diff --git a/src/WorkOS.net/Services/UserManagement/UserManagementService.cs b/src/WorkOS.net/Services/UserManagement/UserManagementService.cs index c9fa1f5b..04ea9632 100644 --- a/src/WorkOS.net/Services/UserManagement/UserManagementService.cs +++ b/src/WorkOS.net/Services/UserManagement/UserManagementService.cs @@ -927,7 +927,7 @@ public virtual async Task CreateOrganizationMembershipAs { if (multiple.RoleSlugs != null) { - request.AddBodyParam("role_slugs", string.Join(",", multiple.RoleSlugs)); + request.AddBodyParam("role_slugs", multiple.RoleSlugs); } } @@ -989,7 +989,7 @@ public virtual async Task UpdateOrganizationMembershipAs { if (multiple.RoleSlugs != null) { - request.AddBodyParam("role_slugs", string.Join(",", multiple.RoleSlugs)); + request.AddBodyParam("role_slugs", multiple.RoleSlugs); } } From 266aee89594d0ee203b370e1cbf8685c2212365b Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 24 Apr 2026 11:54:43 -0400 Subject: [PATCH 3/4] fix: Propagate extra params through auto-pagination ExtraQueryParams and ExtraBodyParams were not copied into subsequent page requests, so paginated endpoints that rely on extra params (e.g. role_slugs filtering) only applied them to the first page. --- src/WorkOS.net/Client/WorkOSClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/WorkOS.net/Client/WorkOSClient.cs b/src/WorkOS.net/Client/WorkOSClient.cs index fdb5f49a..65367bb5 100644 --- a/src/WorkOS.net/Client/WorkOSClient.cs +++ b/src/WorkOS.net/Client/WorkOSClient.cs @@ -291,6 +291,8 @@ public async IAsyncEnumerable ListAutoPagingAsync( AccessToken = request.AccessToken, Options = workingOptions, RequestOptions = request.RequestOptions, + ExtraQueryParams = request.ExtraQueryParams, + ExtraBodyParams = request.ExtraBodyParams, }; string? afterCursor = null; From b5bd628204b4a6852424a730a053d5abc4ddd452 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 24 Apr 2026 11:54:58 -0400 Subject: [PATCH 4/4] fix: Use query params for authorization resource targets Resource target fields (resource_id, resource_external_id, resource_type_slug) were sent as body params, but the authorization endpoint expects them as query params. --- .../Services/Authorization/AuthorizationService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WorkOS.net/Services/Authorization/AuthorizationService.cs b/src/WorkOS.net/Services/Authorization/AuthorizationService.cs index 49738c0f..603bd14c 100644 --- a/src/WorkOS.net/Services/Authorization/AuthorizationService.cs +++ b/src/WorkOS.net/Services/Authorization/AuthorizationService.cs @@ -301,19 +301,19 @@ public virtual async Task RemoveRoleAsync(string organizationMembershipId, Autho { if (byId.ResourceId != null) { - request.AddBodyParam("resource_id", byId.ResourceId); + request.AddQueryParam("resource_id", byId.ResourceId); } } else if (options?.ResourceTarget is AuthorizationResourceTargetByExternalId byExternalId) { if (byExternalId.ResourceExternalId != null) { - request.AddBodyParam("resource_external_id", byExternalId.ResourceExternalId); + request.AddQueryParam("resource_external_id", byExternalId.ResourceExternalId); } if (byExternalId.ResourceTypeSlug != null) { - request.AddBodyParam("resource_type_slug", byExternalId.ResourceTypeSlug); + request.AddQueryParam("resource_type_slug", byExternalId.ResourceTypeSlug); } }