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
NodeListenerwith methods likeonSuccess,onException, andonCallbackReceived. -
New (Coroutines): The new Ping SDK embraces Kotlin Coroutines. Methods like
startandnextaresuspendfunctions. This allows for writing asynchronous code in a sequential, synchronous-looking manner. The different outcomes of an operation are handled by the sealedNodeclass (ContinueNode,SuccessNode,ErrorNode,FailureNode), which allows for exhaustivewhenstatements.
Quick Reference
Method Mapping
| Legacy Method | New Ping Method | Parameter Changes | Return 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()?.accessToken | journey.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 callbacks | Sealed Node types | ContinueNode, SuccessNode, ErrorNode, FailureNode | |
onException(e) callback | is ErrorNode or is FailureNode | Explicit error type discrimination |
Data Model Translation
| Legacy SDK Class | New Ping SDK Model | Description |
|---|---|---|
FRSession | SuccessNode / Journey | The 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. |
Node | ContinueNode | The Node object in the legacy SDK, which contains callbacks for user input, is now represented by a ContinueNode. |
Exception in onException | ErrorNode / FailureNode | Errors and exceptions are now handled through sealed classes ErrorNode (for API errors) and FailureNode (for exceptions). |
Callback | Callback | The Callback classes are similar in both SDKs, but the new SDK has a more structured approach to handling them within the ContinueNode. |
FROptions | JourneyConfig | The SDK initialization options have been streamlined into a new JourneyConfig class with a builder-style configuration. |
UserInfo | UserInfo | The UserInfo model remains, but it is now retrieved synchronously or with coroutines. |
AccessToken | Token | The 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.
| Library | Description |
|---|---|
android | Core Android components for the SDK. |
binding | Used for binding devices to user accounts. |
binding-ui | UI components for device binding. |
browser | Utilities for handling web-based authentication flows. |
commons | Common classes for multi-factor authentication. |
davinci | Allows integration with PingOne DaVinci orchestration flows. |
davinci-plugin | A plugin for extending DaVinci integration. |
device-client | A client for device-related operations. |
device-id | Provides a unique device identifier. |
device-profile | Enables device profiling for risk assessment. |
device-root | Detects if the device is rooted or jailbroken. |
journey | Core library for handling authentication journeys. |
journey-plugin | A plugin for extending journey functionality. |
logger | A logging library for the SDK. |
migration | Assists with migrating from older SDK versions. |
network | Handles network requests for the SDK. |
oath | Implements the OATH (Initiative for Open Authentication) standard. |
oidc | Provides OpenID Connect (OIDC) functionality. |
orchestrate | Handles the orchestration of journey nodes. |
protect | Integrates with PingOne Protect for advanced fraud detection. |
push | Manages push notifications for multi-factor authentication. |
storage | Provides secure storage for SDK data. |
utils | Common utility classes used across the SDK. |