Navigating an authentication journey on Android
PingOne Advanced Identity Cloud PingAM Android
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.
| Node type | Description |
|---|---|
|
Indicates a step in the middle of the authentication journey. Call Can also carry optional page metadata, such as a header and description. See Accessing ContinueNode page metadata. |
|
Represents a bad request, such as invalid credentials. For example a bad password or incorrect one-time passcode (OTP). Access the error message using |
|
Signifies an unexpected error during the authentication process, such as network connectivity issues. Retrieve the underlying cause using |
|
Indicates successful authentication. Obtain the user session details using |
Handle the different node types as follows:
var node = journey.start("sdkUsernamePasswordJourney")
// Determine the type of the current Node
when (node) {
is ContinueNode -> {
// Proceed to the next step in the authentication journey
val nextNode = node.next()
}
is ErrorNode -> {
// Handle server-side errors, such as invalid credentials
val errorMessage = node.message
node.input["name"] // Access the raw JSON response with the input attribute
// Display error to the user
}
is FailureNode -> {
// Handle unexpected errors, such as network issues, parsing response problems
val errorCause = node.cause
// Log the error and potentially display a generic message
}
is SuccessNode -> {
// Authentication successful!
// Retrieve the session
val session = node.session
node.input["name"] // Access the raw JSON response with the input attribute
// Proceed with post-authentication actions
}
}
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:
val node = journey.start() // Initiate the authentication flow
when (node) {
is ContinueNode -> {
node.callbacks.forEach { callback ->
when (callback) {
is NameCallback -> callback.name = "Your Username"
is PasswordCallback -> callback.password = "Your Password"
// Handle other callback types as they are introduced
}
}
// Return the collected input and proceed to the next Node
val nextNode = node.next()
}
is FailureNode -> {/* ... */}
is ErrorNode -> {/* ... */}
is SuccessNode -> {/* ... */}
}
Specific callback types provide their own properties for accessing labels and setting values:
-
NameCallback -
PasswordCallback
val prompt = (callback as? NameCallback)?.prompt // Access the prompt/label
(callback as? NameCallback)?.name = "Your Username" // Set the user’s input
val 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 callbacks it contains.
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:
| Property | Type | Description |
|---|---|---|
|
|
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 |
|
|
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 |
|
|
The raw Either a plain stage name, such as The JSON can contain a
|
|
|
The submit button text resolved from the Not a Page Node field. The Orchestration SDK derives it from the |
|
|
The page footer text resolved from the Not a Page Node field. The Orchestration SDK derives it from the |
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:
{
"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:
-
If the map contains a single value, that value is used regardless of the device locale.
-
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 submitButtonTextmapDevice locale Map key Match? Why en-GBEN-GBNo
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-GBen-gbYes
The device identifier is converted to the lowercase form
en-gb, which matches the key as written.en-GBen_gbYes
The SDK also tries the device identifier with hyphens replaced by underscores, so a key written
en_gbmatches a hyphenated device locale.en_GBen-gbYes
Android converts the device locale to the hyphenated BCP-47 form
en-gbbefore matching, so anen-gbkey matches even though the device locale is written with an underscore. -
-
A language-only match.
Language-only match examples using the pageFootermapDevice locale Map key Match? Why en-CAenYes
No exact match exists, so only the language part of the device locale is compared.
fr-CAenNo
The language part of the device locale,
fr, differs from the map keyen. -
The first value in the map if nothing previously matches.
Android consistently uses the first entry in the stage JSON, so the selection is deterministic.
|
Because of these rules, always write your map keys in lowercase, and use underscores as the separator between language and region subtags, such as
|
When the stage property is absent, is not valid JSON, or has no localized map for that key, submitButtonText and pageFooter are empty strings.
when (node) {
is ContinueNode -> {
if (node.header.isNotEmpty()) {
// Display node.header above the form
}
if (node.description.isNotEmpty()) {
// Display node.description below the header
}
// Render node.callbacks as before
Button(
onClick = { /* Call node.next() in a coroutine / },
) {
Text(node.submitButtonText.ifEmpty { "Continue" })
}
if (node.pageFooter.isNotEmpty()) {
// Display node.pageFooter below the button
}
}
else -> { / ... \*/ }
}
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
Throwablethat caused the error usingnode.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
4xxor5xxstatus 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 usingnode.input3.
val node = journey.start() // Initiate the authentication flow
when (node) {
is ContinueNode -> {/* ... */}
is FailureNode -> {
// Retrieve the underlying error
val errorCause = node.cause (1)
// Log the errorCause for debugging
// Display a generic error message to the user
}
is ErrorNode -> {
// Retrieve the server-provided error message
val errorMessage = node.message (2)
// Access the raw JSON error response
val rawResponse = node.input (3)
// Display the specific error message to the user
}
is SuccessNode -> {/* ... */}
}
Handling SuccessNode
When the Journey module reaches the SuccessNode type it securely stores session tokens, and populates the user object.
val node = journey.start() // Initiate the authentication flow
when (node) {
is ContinueNode -> {/* ... */}
is FailureNode -> {/* ... */}
is ErrorNode -> {/* ... */}
is SuccessNode -> {
// Checking the user object
val user: User? = journey.user()
// Retrieve the session token, if available
val session: SSOToken? = user?.ssoToken()
}
}