Orchestration SDKs

Navigating an authentication journey on iOS

PingOne Advanced Identity Cloud PingAM iOS

The start() method returns a Node instance, which represents where you are in the authentication journey.

There are four main types of Node instance a journey can return.

The four different types of node instances
Node type Description

ContinueNode

Indicates a step in the middle of the authentication journey.

Call node.next() to submit any collected data and advance to the next node.

Can also carry optional page metadata, such as a header and description. See Accessing ContinueNode page metadata.

ErrorNode

Represents a bad request, such as invalid credentials. For example a bad password or incorrect one-time passcode (OTP).

Access the error message using node.message.

FailureNode

Signifies an unexpected error during the authentication process, such as network connectivity issues.

Retrieve the underlying cause using node.cause.

SuccessNode

Indicates successful authentication.

Obtain the user session details using node.session.

Handle the different node types as follows:

Handling the different node types in a journey
let node = await journey.start("sdkUsernamePasswordJourney")

// Determine the type of the current Node
switch node {
    case let continueNode as ContinueNode:
        // Proceed to the next step in the authentication journey
        let nextNode = continueNode.next()

    case let errorNode as ErrorNode:
        // Handle server-side errors, such as invalid credentials
        let errorMessage = errorNode.message
        let input = errorNode.input // Access the raw JSON response with the input attribute
        // Display error to the user

    case let failureNode as FailureNode:
        // // Handle unexpected errors, such as network issues, parsing response problems
        let errorCause = failureNode.cause
        // Log the error and potentially display a generic message

    case let successNode as SuccessNode:
        // Authentication successful!
        // Retrieve the session
        let session = successNode.session
        let input = successNode.input // Access the raw JSON response with the input attribute
        // Proceed with post-authentication actions

    default:
        break
}

Handling ContinueNode

The ContinueNode type often contains a list of callbacks that require input from the client.

You can access these using node.callbacks(), and provide the necessary information to each relevant callback. Ensure you call next() to submit the collected data and traverse through the journey nodes.

Handling different callbacks
if let current = node as? ContinueNode {
      current.callbacks.forEach { callback in
          switch callback {
           case let nameCallback as NameCallback:
              nameCallback.name = "Your Username"
          case let passwordCallback as PasswordCallback:
              passwordCallback.password = "Your Password"
              // Handle other callback types as they are introduced
          default:
              break
          }
     }
}

// Proceed to the next Node with the provided input
let nextNode = current.next()

Specific callback types provide their own properties for accessing labels and setting values:

Accessing callback prompts and setting input data
  • NameCallback

  • PasswordCallback

let prompt = (callback as? NameCallback)?.prompt // Access the prompt/label
(callback as? NameCallback)?.name = "Your Username" // Set the user’s input
let prompt = (callback as? PasswordCallback)?.prompt // Access the prompt/label
(callback as? PasswordCallback)?.password = "Your Password" // Set the user’s password

Learn about supported callbacks in Supported Nodes and Callbacks.

Accessing ContinueNode page metadata

A Page Node in an authentication journey can carry UI metadata in addition to the nodes, and their callbacks within it.

You set this metadata on the Page Node when you configure the journey. See the Page node configuration reference for the available properties.

When the server sends a Page Node, the Orchestration SDK surfaces this metadata as properties on the ContinueNode, so you can use it to render the step:

ContinueNode page metadata properties
Property Type Description

continueNode.pageHeader

String

Header text to display above the form, if the server set one on the Page Node.

A plain string, taken directly from the Page Node’s header field. No localized map support.

continueNode.pageDescription

String

Description text to display below the header, if the server set one on the Page Node.

A plain string, taken directly from the Page Node’s description field. No localized map support.

continueNode.stage

String

The raw stage value set on the Page Node.

Either a plain stage name, such as Login, or a JSON string that carries localized UI text.

The JSON can contain a submitButtonText and a pageFooter map:

{
  "submitButtonText": {
    "en": "Continue",
    "en-gb": "Sign in"
  },
  "pageFooter": {
    "en": "© 2026 Example Inc, USA",
    "fr": "© 2026 Exemple SA"
  }
}

continueNode.submitButtonText

String

The submit button text resolved from the stage locale map using the device locale.

continueNode.pageFooter

String

The page footer text resolved from the stage locale map using the device locale.

Each of these properties is a string. If the server does not provide the underlying value, whether set directly on the Page Node or resolved from the stage JSON, then the property value is set to an empty string. This means that you can treat each one as a plain string without checking its type.

Resolving localized text from the stage locale map

When the Page Node’s stage property is a JSON string, it can carry localized values for the submit button text and the page footer:

Example stage JSON with localized UI text
{
  "submitButtonText": {
    "en": "Continue",
    "en-gb": "Sign in",
    "fr": "Continuer"
  },
  "pageFooter": {
    "en": "© 2026 Example Inc, USA",
    "fr": "© 2026 Exemple SA",
    "fr-CA": "© 2026 Example Corp, Canada"
  }
}

The Orchestration SDK resolves submitButtonText and pageFooter text values from this map by comparing the map key against the locale set on the device, in the following order:

  1. If the map contains a single value, that value is used regardless of the device locale.

  2. If there are multiple values in a map, the Orchestration SDK attempts a text match with the following process:

    • The device locale identifier is always converted to lowercase.

    • The SDK creates a variant of the device locale to attempt a match, with hyphens replaced by underscores.

    • Map keys are compared as written and are not modified in any way.

    Text match examples using the submitButtonText map
    Device locale Map key Match? Why

    en-GB

    EN-GB

    No

    Map keys are not modified before comparison, so a key that is not lowercase never matches a device locale identifier, which is always converted to lowercase.

    en-GB

    en-gb

    Yes

    The device identifier is converted to the lowercase form en-gb, which matches the key as written.

    en-GB

    en_gb

    Yes

    The SDK also tries the device identifier with hyphens replaced by underscores, so a key written en_gb matches a hyphenated device locale.

    en_GB

    en-gb

    No

    iOS device locale identifiers use underscores, so the identifier is the lowercase form en_gb.

    The SDK only generates an underscore variant of the identifier, never a hyphenated one, so the key en-gb never matches.

  3. A language-only match.

    Language-only match examples using the pageFooter map
    Device locale Map key Match? Why

    en-CA

    en

    Yes

    No exact match exists, so only the language part of the device locale is compared.

    fr-CA

    en

    No

    The language part of the device locale, fr, differs from the map key en.

  4. An arbitrary value in the map if nothing matches. The selected value might be different on each execution of the app.

Because of these rules, always write your map keys in lowercase, and use underscores as the separator between the language and region subtags, such as en_gb.

  • Underscore keys match in every device locale configuration.

  • Hyphenated keys such as en-gb can only match when the device locale identifier itself is hyphenated, which is uncommon on iOS, so another reason to prefer underscores.

When the stage property is absent, is not valid JSON, or has no localized map for that key, submitButtonText and pageFooter are empty strings.

Rendering ContinueNode page metadata
if let continueNode = node as? ContinueNode {
    if !continueNode.pageHeader.isEmpty {
        // Display continueNode.pageHeader above the form
    }

    if !continueNode.pageDescription.isEmpty {
        // Display continueNode.pageDescription below the header
    }

    // Render continueNode.callbacks as before

    Button(continueNode.submitButtonText.isEmpty ? "Continue" : continueNode.submitButtonText) {
        Task { await continueNode.next() }
    }

    if !continueNode.pageFooter.isEmpty {
        // Display continueNode.pageFooter below the button
    }
}

Handling FailureNode and ErrorNode

The Journey module distinguishes between the FailureNode and ErrorNode types for different categories of errors encountered during the authentication flow.

FailureNode

Indicates an unexpected issue that prevents the journey from continuing. This could stem from network problems or data parsing errors.

You can access the underlying Throwable that caused the error using node.cause() 1.

You should display a user-friendly generic error message and log the details for support investigation.

ErrorNode

Signifies an error response from the authentication server, typically an HTTP 4xx or 5xx status code.

These errors often relate to invalid user input or issues server-side.

You can retrieve the specific error message provided by the server using node.message() 2, and access the raw JSON response by using node.input 3.

Handling FailureNodes and ErrorNodes
let node = await journey.start("sdkUsernamePasswordJourney")

switch node {
    case let continueNode as ContinueNode:
        // ...
        break

    case let failureNode as FailureNode:
        // Retrieve the underlying error
        let errorCause = failureNode.cause (1)
        // Log the errorCause for debugging
        // Display a generic error message to the user

    case let errorNode as ErrorNode:
        // Retrieve the server-provided error message
        let errorMessage = errorNode.message (2)
        // Access the raw JSON error response
        let rawResponse = errorNode.input (3)
        // Display the specific error message to the user

    case let successNode as SuccessNode:
        // ...
        break

    default:
        break
}

Handling SuccessNode

WHen the Journey module reaches the SuccessNode type it securely stores the session token. Use the journeyUser() function to create an object representing the user, and call token() on that object to obtain the session token itself:

Handling SuccessNode
let node = await journey.start("sdkUsernamePasswordJourney")

switch node {
    case let continueNode as ContinueNode:
        let token: Result<Token, OidcError>?
        // Checking the user object
        let journeyUser = await journey.journeyUser()

        // Retrieve the session token, if available
        token = journeyUser.token()
        break

    case let failureNode as FailureNode:
        // ...
        break

    case let errorNode as ErrorNode:
        // ...
        break

    case let successNode as SuccessNode:
        // ...
        break

    default:
        break
}

If you integrated the OIDC module, it provides methods to interact with OpenID Connect tokens, such as obtaining data from the user info endpoint, or revoking the access token:

Managing OpenID Connect tokens with the OIDC module integrated
let node = await journey.start("sdkUsernamePasswordJourney")

switch node {
    case let continueNode as ContinueNode:
        let token: Result<Token, OidcError>?
        // Checking the user object
        let journeyUser = await journey.journeyUser()

        // Retrieve the session token, if available
        token = journeyUser.token()

        switch token {
            case .success(let token):
                await MainActor.run {
                    self.token = String(describing: token)
                    let accessToken = token.accessToken
                }
                LogManager.standard.i("AccessToken: \(self.token.accessToken)")
            case .failure(let error):
                await MainActor.run {
                    self.token = "Error: \(error.localizedDescription)"
                }
                LogManager.standard.e("", error: error)
            case .none:
                break
            }
        }

        // Fetch user information using the access token (if valid)
        journeyUser.userinfo()

        // Revoke the current access and refresh tokens
        journeyUser.revoke()

        // Initiate the logout process, potentially clearing local session data
        journeyUser.logout()
        break

    case let failureNode as FailureNode:
        // ...
        break

    case let errorNode as ErrorNode:
        // ...
        break

    case let successNode as SuccessNode:
        // ...
        break

    default:
        break
}