Journey Module Migration: Forgerock to Ping SDK

February 10, 2026 ยท View on GitHub

This document provides a comprehensive mapping of the Journey module from the legacy Forgerock SDK to the new Ping SDK. It is intended to be used as a reference for refactoring and migration efforts. The examples are based on real-world implementations and demonstrate the key architectural and API changes.

Migration Overview

The primary architectural shift is the move from a callback-based asynchronous model to a modern, coroutine-based approach.

  • Legacy (Callbacks): The legacy SDK used a NodeListener with methods like onSuccess, onException, and onCallbackReceived.

  • New (Coroutines): The new Ping SDK embraces Kotlin Coroutines. Methods like start and next are suspend functions. This allows for writing asynchronous code in a sequential, synchronous-looking manner. The different outcomes of an operation are handled by the sealed Node class (ContinueNode, SuccessNode, ErrorNode, FailureNode), which allows for exhaustive when statements.

Quick Reference

Method Mapping

Legacy MethodNew Ping MethodParameter ChangesReturn Type
FRSession.authenticate(context, journeyName, listener)journey.start(journeyName)- context is no longer passed directly to every method.
- The listener is replaced by the suspend function's return value.
void (asynchronous with listener) -> Node (synchronous-style with coroutines)
node.next(context, listener)continueNode.next()- context is no longer passed directly.
- The listener is replaced by the suspend function's return value.
void (asynchronous with listener) -> Node (synchronous-style with coroutines)
FRUser.getCurrentUser()?.logout()journey.user()?.logout()The new SDK provides a nullable user object from the journey, on which logout can be called.void -> void
FRUser.getCurrentUser()?.getUserInfo(...)user.userinfo()The FRListener is replaced by a Result object.void (asynchronous with listener) -> Result<UserInfo, Exception>
FRUser.getCurrentUser()?.accessTokenjourney.user()?.token()The FRListener is replaced by a Result object.void (asynchronous with listener) -> Result<Token, OidcError>
FRUser.getCurrentUser()?.revokeAccessToken(...)journey.user()?.revoke()The FRListener is replaced by a suspend function.void (asynchronous with listener) -> suspend function
FRUser.getCurrentUser()?.refreshAccessToken(...)journey.user()?.refresh()The FRListener is replaced by a Result object.void (asynchronous with listener) -> Result<Token, OidcError>
NodeListener callbacksSealed Node typesContinueNode, SuccessNode, ErrorNode, FailureNode
onException(e) callbackis ErrorNode or is FailureNodeExplicit error type discrimination

Data Model Translation

Legacy SDK ClassNew Ping SDK ModelDescription
FRSessionSuccessNode / JourneyThe FRSession object, which represents a successful login, is now represented by a SuccessNode returned by the journey. The Journey object itself holds the session state.
NodeContinueNodeThe Node object in the legacy SDK, which contains callbacks for user input, is now represented by a ContinueNode.
Exception in onExceptionErrorNode / FailureNodeErrors and exceptions are now handled through sealed classes ErrorNode (for API errors) and FailureNode (for exceptions).
CallbackCallbackThe Callback classes are similar in both SDKs, but the new SDK has a more structured approach to handling them within the ContinueNode.
FROptionsJourneyConfigThe SDK initialization options have been streamlined into a new JourneyConfig class with a builder-style configuration.
UserInfoUserInfoThe UserInfo model remains, but it is now retrieved synchronously or with coroutines.
AccessTokenTokenThe FRListener is replaced by a Result object.

Example: SDK Initialization

Legacy

 val PingAM = FROptionsBuilder.build {
    server {
        url = "https://iam.dev.thrivent.com/am"
        realm = "alpha"
        cookieName = "c4891df37ce0971"
        timeout = 50
    }
    oauth {
        oauthClientId = "PingTest"
        oauthRedirectUri = "org.forgerock.demo://oauth2redirect"
        oauthScope = "openid profile email address"
        oauthSignOutRedirectUri = "org.forgerock.demo://oauth2redirect"
    }
    service {
        authServiceName = "sdkUsernamePasswordJourney"
    }
}

Modern

val journey = Journey {
    logger = Logger.STANDARD
    serverUrl = "https://openam-sdks.forgeblocks.com/am"
    realm = "alpha"
    cookie = "5421aeddf91aa20"
    // Oidc as module
    module(Oidc) {
        clientId = "AndroidTest"
        discoveryEndpoint =
            "https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration"
        scopes = mutableSetOf("openid", "email", "address", "profile", "phone")
        redirectUri = "org.forgerock.demo:/oauth2redirect"
        //storage = dataStore
    }
}

Example: Starting Authentication and Handling Nodes

Legacy

private val nodeListener = object : NodeListener<FRSession> {
    override fun onSuccess(result: FRSession) {
        // Handle successful login
        logger.i("Authentication successful")
        processCallbacks(node, nodeListener)
    }

    override fun onException(e: Exception) {
        // Handle error
        logger.e("Authentication failed", e)
    }

    override fun onCallbackReceived(node: Node) {
        // Process node and set callbacks
        processCallbacks(node)
    }
}
FRSession.authenticate(context, "Login", nodeListener)

Modern

var node: Node = journey.start("Login")

when (node) {
    is ContinueNode -> { 
        processCallbacks(node)
        node.next()
    }
    is SuccessNode -> { 
        println("Authentication successful") 
    }
    is ErrorNode -> {
        println("Authentication failed. ${node.message}") 
    }
    is FailureNode -> { 
        println("Failed to authenticate. ${node.cause}") 
    }
}

Move to next node in the Journey

Legacy

fun processCallbacks(node: Node, nodeListener: NodeListener<FRSession>) {
    node.callbacks?.forEach {
        if (it is NameCallback) {
            it.setName("username")
        } else if (it is PasswordCallback) {
            it.setPassword("password".toCharArray())
        }
    }
    node.next(context, nodeListener)
}

Modern

fun processCallbacks(node: ContinueNode) {
    callbacks.forEach { callback ->
        when (callback) {
            is NameCallback -> callback.name = "username"
            is PasswordCallback -> callback.password = "password"
        }
    }
}

Retrieving User Profile

Legacy

FRUser.getCurrentUser()?.getUserInfo(object : FRListener<UserInfo> {
    override fun onSuccess(result: UserInfo) { /* ... */ }
    override fun onException(e: Exception) { /* ... */ }
})

Modern

when (val result = user.userinfo(false)) {
    is Result.Failure -> { /* ... */ }
    is Result.Success -> { /* ... */ }
}

Access Token Management

Legacy

// Get Access Token
val accessToken = FRUser.getCurrentUser()?.accessToken

// Refresh Token
FRUser.getCurrentUser()?.refresh(object : FRListener<AccessToken?> {
    override fun onSuccess(result: AccessToken?) { /* ... */ }
    override fun onException(e: Exception) { /* ... */ }
})

Modern

// Get Access Token
val token = journey.user()?.token()

// Refresh Token
val result: Result<Token, OidcError> = journey.user()?.refresh()

User Logout

Legacy

FRUser.getCurrentUser()?.logout()

Modern

journey.user()?.logout()

Example: Social Login

Configuration should be done on the server to enable different IDP such as apple, google, facebook.

IDP Callback

Legacy

callback.signIn(context, handler, object : FRListener<String> {
    override fun onSuccess(result: String) {
        node.next(context, nodeListener)
    }
    override fun onException(e: Exception) {
        logger.e("Sign-in failed", e)
    }
})

Modern

callback.authorize(redirectUri) { result ->
    when (result) {
        is Result.Success -> { idpResult
            logger.i("Sign in successful ${idpResult.token}")
            node = node.next()
        }
        is Result.Failure -> {
            logger.e("IDP sign-in failed", result.error)
        }
    }
}

Example: WebAuthn Registration

Legacy

val callback = WebAuthRegistrationCallback()
callback.setResidentKeyRequirement(ResidentKeyRequirement.RESIDENT_KEY_DISCOURAGED)
callback.register(context, deviceName, node)

Modern

val callback = FidoRegistrationCallback()
callback.register(deviceName)
    .onSuccess { result ->
        logger.i("WebAuthn registration successful")
    }
    .onFailure { error ->
        logger.e("WebAuthn registration failed", error)
    }

Example: WebAuthn Authentication

Legacy

val callback = WebAuthAuthenticationCallback()
callback.authenticate(context, deviceName, node)

Modern

val callback = FidoAuthenticationCallback()
callback.authenticate()
    .onSuccess { result ->
        logger.i("WebAuthn authentication successful")
    }
    .onFailure { error ->
        logger.e("WebAuthn authentication failed", error)
    }

Example: Device Binding Callback

Legacy

val callback = DeviceBindingCallback()
callback.bind(context, deviceName, object : FRListener<String> {
    override fun onSuccess(result: String) {
        logger.i("Device bound successfully")
    }
    override fun onException(e: Exception) {
        logger.e("Device binding failed", e)
    }
})

Modern

callback.bind {
    this.deviceName = deviceName
    // Optional configuration
}.onFailure {
    logger.e("Device binding failed", it)
}

Example: Device Profiling Callback

Legacy

public void deviceCollector() {
    FRDeviceCollectorBuilder builder = FRDeviceCollector.builder();
    if (metadata) {
        builder.collector(new MetadataCollector());
    }
    if (location) {
        builder.collector(new LocationCollector());
    }

    builder.build().collect(context, new FRListener<JSONObject>() {
        @Override
        public void onSuccess(JSONObject result) {
            setValue(result.toString());
            Listener.onSuccess(listener, null);
        }

        @Override
        public void onException(Exception e) {
            Listener.onException(listener, e);
        }
    });
}

Modern

deviceProfileCallback.collect {
    collectors.apply(DefaultDeviceCollector())
}

Example: Device Identifier

Legacy

DeviceIdentifier.builder().context(applicationContext).build().identifier

Modern

DefaultDeviceIdentifier.id()

Example: PingOne Protect

Initialization

Legacy

if (callback is PingOneProtectInitializeCallback) {
    try {
        callback.start(context)
    } catch (e: PingOneProtectInitException) {
        Logger.error("PingOneInitException", e, e.message)
    } catch (e: Exception) {
        Logger.error("PingOneInitException", e, e.message)
    }    
}

Modern

val callback = PingOneProtectInitializeCallback()
callback.start().onSuccess {
    logger.i("PingOne Protect initialization successful")
}.onFailure { error ->
    logger.e("PingOne Protect initialization failed", error)
}

Evaluation

if (callback is PingOneProtectEvaluationCallback) {
    callback.getData(context)
}

Modern

val callback = PingOneProtectEvaluationCallback()
callback.collect().onSuccess {
    logger.i("PingOne Protect evaluation successful")
}.onFailure { error ->
    logger.e("PingOne Protect evaluation failed", error)
}

Example: ReCAPTCHA Enterprise

Legacy

val callback = ReCaptchaEnterpriseCallback()
callback.execute(application = application)

Modern

val reCaptchaEnterpriseCallback = ReCaptchaEnterpriseCallback()
reCaptchaEnterpriseCallback.verify {
    // Optionally customize the configuration here
    // config.payload = mapOf("custom_key" to "custom_value")
}.onSuccess { result ->
    logger.i("ReCAPTCHA Token Result: $result")
    onNext() // Proceed to next step 
}.onFailure { error ->
    logger.e("ReCAPTCHA Verification Failed: ${error.message}", error)
    onNext() // Proceed to next step (or handle error differently)
}

Example: Resume Authentication Flow (Suspended Email Node)

Legacy

// In MainActivity or Activity handling deep links
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val resumeUri = intent?.data // Contains 'suspendedId' parameter
    if (resumeUri != null) {
        // Resume authentication with URI
        FRSession.authenticate(context, resumeUri, nodeListener)
    }
}

private val nodeListener = object : NodeListener<FRSession> {
    override fun onSuccess(result: FRSession) {
        logger.i("Authentication successful after resume")
        navigateToHome()
    }
    override fun onCallbackReceived(node: Node) {
        // Handle node
        node.next(context, this)
    }
    override fun onException(e: Exception) {
        logger.e("Resume authentication failed", e)
    }
}

Modern

// In Activity or ViewModel handling deep links
intent?.data?.let { resumeUri ->
    try {
        // Resume authentication flow
        var node: Node = journey.resume(uri = resumeUri)

        // Process callbacks
        node = node.next()

        when (node) {
            is SuccessNode -> {
                logger.i("Authentication successful after resume")
            }
            is ErrorNode -> {
                logger.w("Resume authentication error: ${node.errorMessage}")
            }
            is FailureNode -> {
                logger.e("Resume authentication failed", node.exception)
            }
        }
    } catch (e: Exception) {
        logger.e("Resume authentication exception", e)
    }
}

Example: Checking Current User Session

Legacy

// Check if user is authenticated
val currentUser = FRUser.getCurrentUser()
if (currentUser != null) {
    logger.i("User is authenticated: ${currentUser.id}")
    // User is logged in
} else {
    logger.i("No authenticated user")
    // Show login screen
}

// Get user info
currentUser?.getUserInfo(object : FRListener<UserInfo> {
    override fun onSuccess(result: UserInfo) {
        logger.i("User info: ${result.name}")
    }
    override fun onException(e: Exception) {
        logger.e("Failed to get user info", e)
    }
})

Modern

journey.user()?.let {
    logger.i("User is authenticated: ${user.id}")
    // User is logged in

    // Get user info asynchronously
    when (val result = user.userinfo(false)) {
        is Result.Success -> {
            val userInfo = result.value
            logger.i("User info: ${userInfo.name}")
        }
        is Result.Failure -> {
            logger.e("Failed to get user info", result.error)
        }
    }
} ?: run {
    logger.w("No authenticated user")
    null
}

Example: Centralized Login (Browser-based OIDC)

Legacy

FRUser.browser().appAuthConfigurer().customTabsIntent {
    it.setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK)
}.appAuthConfiguration { appAuthConfiguration ->
    // Additional configuration
}
.done()
.login(fragmentActivity,
    object : FRListener<FRUser> {
        override fun onSuccess(result: FRUser) {
            logger.i("Browser login successful")
        }
        override fun onException(e: Exception) {
            logger.e("Browser login failed", e)
        }
    })

Modern

oidcWeb.authorize {
    // Additional configuration
}.onSuccess { user ->
        logger.i("Browser login successful")
    }
    .onFailure { error ->
        logger.e("Browser login failed", error)
    }

Example: Getting Access Token After Session

Legacy

FRUser.getCurrentUser()?.getAccessToken(object : FRListener<AccessToken> {
    override fun onSuccess(result: AccessToken) {
        logger.i("Token retrieved ${result.value}")
    }

    override fun onException(e: Exception) {
        logger.e("Failed to get access token", e)
    }
})

Modern

val user = journey.user()?.let {
    when (val result = it.token()) {
        is Failure -> {
            logger.e("Failed to get access token")
        }
        is Success -> {
            logger.i("Token retrieved ${result.value}")
        }
    }
}

Configuration: Gradle Dependencies

To integrate the new Ping Identity SDK, update your build.gradle.kts with dependencies. The SDK is modular, so include only what you need.

Dependencies

dependencies {
    // Core SDK
    implementation(libs.ping.sdk.journey)
    implementation(libs.ping.sdk.orchestrate)

    // Optional Modules
    implementation(libs.ping.sdk.oidc) // For OIDC
    implementation(libs.ping.sdk.device.profile) // For device profiling
    implementation(libs.ping.sdk.binding) // For device binding
    implementation(libs.ping.sdk.device.id) // For device ID
    implementation(libs.ping.sdk.device.root) // For device root detection
    implementation(libs.ping.sdk.push) // For push notifications
    implementation(libs.ping.sdk.protect) // For PingOne Protect
    implementation(libs.ping.sdk.davinci) // For DaVinci integration

    // UI and other utilities
    implementation(libs.ping.sdk.android)
    implementation(libs.ping.sdk.browser)
    implementation(libs.ping.sdk.commons)
    implementation(libs.ping.sdk.logger)
    implementation(libs.ping.sdk.network)
    implementation(libs.ping.sdk.storage)
    implementation(libs.ping.sdk.utils)
}

Example libs.versions.toml

[versions]
ping-sdk = "2.0.0-beta1"

[libraries]
ping-sdk-journey = { group = "com.pingidentity.sdks", name = "journey", version.ref = "ping-sdk" }
ping-sdk-orchestrate = { group = "com.pingidentity.sdks", name = "orchestrate", version.ref = "ping-sdk" }
ping-sdk-oidc = { group = "com.pingidentity.sdks", name = "oidc", version.ref = "ping-sdk" }
ping-sdk-device-profile = { group = "com.pingidentity.sdks", name = "device-profile", version.ref = "ping-sdk" }
ping-sdk-binding = { group = "com.pingidentity.sdks", name = "binding", version.ref = "ping-sdk" }
ping-sdk-push = { group = "com.pingidentity.sdks", name = "push", version.ref = "ping-sdk" }
ping-sdk-protect = { group = "com.pingidentity.sdks", name = "protect", version.ref = "ping-sdk" }
ping-sdk-davinci = { group = "com.pingidentity.sdks", name = "davinci", version.ref = "ping-sdk" }
ping-sdk-browser = { group = "com.pingidentity.sdks", name = "browser", version.ref = "ping-sdk" }
ping-sdk-utils = { group = "com.pingidentity.sdks", name = "utils", version.ref = "ping-sdk" }
ping-sdk-logger = { group = "com.pingidentity.sdks", name = "logger", version.ref = "ping-sdk" }
ping-sdk-storage = { group = "com.pingidentity.sdks", name = "storage", version.ref = "ping-sdk" }
ping-sdk-network = { group = "com.pingidentity.sdks", name = "network", version.ref = "ping-sdk" }
ping-sdk-device-id = { group = "com.pingidentity.sdks", name = "device-id", version.ref = "ping-sdk" }
ping-sdk-device-root = { group = "com.pingidentity.sdks", name = "device-root", version.ref = "ping-sdk" }
ping-sdk-device-profile = { group = "com.pingidentity.sdks", name = "device-profile", version.ref = "ping-sdk" }
ping-sdk-migration = { group = "com.pingidentity.sdks", name = "migration", version.ref = "ping-sdk" }
ping-sdk-oath = { group = "com.pingidentity.sdks", name = "oath", version.ref = "ping-sdk" }
ping-sdk-device-client = { group = "com.pingidentity.sdks", name = "device-client", version.ref = "ping-sdk" }
ping-sdk-binding-ui = { group = "com.pingidentity.sdks", name = "binding-ui", version.ref = "ping-sdk" }
ping-sdk-journey-plugin = { group = "com.pingidentity.sdks", name = "journey-plugin", version.ref = "ping-sdk" }
ping-sdk-davinci-plugin = { group = "com.pingidentity.sdks", name = "davinci-plugin", version.ref = "ping-sdk" }
ping-sdk-commons = { group = "com.pingidentity.sdks", name = "commons", version.ref = "ping-sdk" }
ping-sdk-android = { group = "com.pingidentity.sdks", name = "android", version.ref = "ping-sdk" }

Available Libraries

The following libraries are available in the Ping Identity SDK. You can find the latest versions on Maven Central.

LibraryDescription
androidCore Android components for the SDK.
bindingUsed for binding devices to user accounts.
binding-uiUI components for device binding.
browserUtilities for handling web-based authentication flows.
commonsCommon classes for multi-factor authentication.
davinciAllows integration with PingOne DaVinci orchestration flows.
davinci-pluginA plugin for extending DaVinci integration.
device-clientA client for device-related operations.
device-idProvides a unique device identifier.
device-profileEnables device profiling for risk assessment.
device-rootDetects if the device is rooted or jailbroken.
journeyCore library for handling authentication journeys.
journey-pluginA plugin for extending journey functionality.
loggerA logging library for the SDK.
migrationAssists with migrating from older SDK versions.
networkHandles network requests for the SDK.
oathImplements the OATH (Initiative for Open Authentication) standard.
oidcProvides OpenID Connect (OIDC) functionality.
orchestrateHandles the orchestration of journey nodes.
protectIntegrates with PingOne Protect for advanced fraud detection.
pushManages push notifications for multi-factor authentication.
storageProvides secure storage for SDK data.
utilsCommon utility classes used across the SDK.