Skip to content

Regression in 2.1.5: Remediation.proceed() drops sibling fields in nested forms (breaks email/SMS challenge) #276

Description

@ag08291

Describe the bug?

In okta-mobile-swift 2.1.5, calling Remediation.proceed() on any IDX remediation whose form contains a named field with multiple nested children serializes only the last child — sibling fields are silently dropped. This breaks the standard authenticator-verification-data and select-authenticator-authenticate flows because the immutable required id field gets clobbered, and the Okta server rejects the request with HTTP 400.
Pinning back to 2.1.4 resolves the issue.

Root cause

The regression is in Sources/OktaIdxAuth/Internal/Extensions/IDXRemediation+Extensions.swift, where
Remediation.Form.Field.formValue was rewritten between 2.1.4 and 2.1.5.

2.1.4 (correct) accumulates all child fields into a dict, then wraps under the parent name:

let childValues: [String: any JSONRepresentable] = try form.allFields.reduce(into: [:]) { (result, formValue) in
    let nestedResult = try formValue.formValues()
    ...
    if let name = formValue.name {
        result[name] = nestedResult
    }
    ...
}
result = try JSON([name: childValues])

2.1.5 (broken) overwrites the same key on every loop iteration:

  if let form = self.form, !form.allFields.isEmpty {
      for field in form.allFields {
          guard let nestedResult = try field.formValue else { continue }
          json[name] = nestedResult   // ← overwrites previous sibling each pass
      }
  }

Walking the loop for an authenticator-verification-data form:

  1. field = id → nestedResult = {"id": ""} → json["authenticator"] = {"id": "..."}
  2. field = methodType (with selectedOption = "email") → nestedResult = {"methodType": "email"} → json["authenticator"] = {"methodType": "email"} ← clobbers the previous entry

The body that reaches the server contains only methodType. The fix is to merge nested results under the parent key instead of overwriting (mirroring the reduce(into:) accumulation in 2.1.4).

Impact

This breaks Remediation.proceed() for any IDX remediation that has a named field with two or more nested children — i.e. essentially every multi-factor challenge flow (authenticator-verification-data, select-authenticator-authenticate, challenge-authenticator for factors that send id+methodType, etc.).

We caught it via Proxyman captures: identical Okta server responses produce different request bodies between a 2.1.4-linked binary (works) and a 2.1.5-linked binary (400s on every challenge attempt).

Workaround
Pin to exactVersion 2.1.4 until this is fixed.

What is expected to happen?

Calling Remediation.proceed() on an authenticator-verification-data remediation (with nested authenticator form containing id + methodType siblings) should serialize all sibling fields under the parent key, producing:

{
  "authenticator": { "id": "<authenticator-id>", "methodType": "email" },
  "stateHandle": "<state-handle>"
}

Okta returns 200 OK and the flow progresses to challenge-authenticator. This is the 2.1.4 behavior.

What is the actual behavior?

Only the last sibling field is serialized — earlier siblings are silently overwritten in the JSON dictionary. The actual request body is:

{
  "stateHandle": "<state-handle>",
  "authenticator": { "methodType": "email" }
}

Okta returns 400 Bad Request because id is required and immutable in the form schema. The flow is unrecoverable from the client side, breaking every multi-factor challenge.

Reproduction Steps?

  1. Start an Interaction Code Flow against an Okta tenant where the user has an email authenticator enrollment.
  2. Drive the flow through interactintrospectidentify so the response contains an authenticator-verification-data remediation with the standard nested form:
{
  "name": "authenticator-verification-data",
  "value": [
    {
      "name": "authenticator",
      "form": {
        "value": [
          { "name": "id", "required": true, "value": "<authenticator-id>", "mutable": false },
          { "name": "methodType", "type": "string", "required": true, "options": [{ "label": "Email", "value": "email"
}] }
        ]
      }
    },
    { "name": "stateHandle", "required": true, "value": "<state-handle>", "visible": false, "mutable": false }
  ]
}
  1. Set methodTypeField.selectedOption to the email option and call remediation.proceed().
  2. Capture the outgoing HTTP request (e.g. via Proxyman). Observe that the authenticator object is missing the id field, and the server responds with HTTP 400.
  3. Repeat with the dependency pinned to exactVersion 2.1.4 — the request body now contains both id and methodType, and the server returns 200 OK.

Additional Information?

No response

SDK Version(s)

2.1.5 (regression) — last working version: 2.1.4
Product: OktaIdxAuth
Platform: iOS (SPM)

Build Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions