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.
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.
| 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 |
| 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. |
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"
}
}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
}
}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)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}")
}
}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)
}fun processCallbacks(node: ContinueNode) {
callbacks.forEach { callback ->
when (callback) {
is NameCallback -> callback.name = "username"
is PasswordCallback -> callback.password = "password"
}
}
}FRUser.getCurrentUser()?.getUserInfo(object : FRListener<UserInfo> {
override fun onSuccess(result: UserInfo) { /* ... */ }
override fun onException(e: Exception) { /* ... */ }
})when (val result = user.userinfo(false)) {
is Result.Failure -> { /* ... */ }
is Result.Success -> { /* ... */ }
}// 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) { /* ... */ }
})// Get Access Token
val token = journey.user()?.token()
// Refresh Token
val result: Result<Token, OidcError> = journey.user()?.refresh()FRUser.getCurrentUser()?.logout()journey.user()?.logout()Configuration should be done on the server to enable different IDP such as apple, google, facebook.
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)
}
})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)
}
}
}val callback = WebAuthRegistrationCallback()
callback.setResidentKeyRequirement(ResidentKeyRequirement.RESIDENT_KEY_DISCOURAGED)
callback.register(context, deviceName, node)val callback = FidoRegistrationCallback()
callback.register(deviceName)
.onSuccess { result ->
logger.i("WebAuthn registration successful")
}
.onFailure { error ->
logger.e("WebAuthn registration failed", error)
}val callback = WebAuthAuthenticationCallback()
callback.authenticate(context, deviceName, node)val callback = FidoAuthenticationCallback()
callback.authenticate()
.onSuccess { result ->
logger.i("WebAuthn authentication successful")
}
.onFailure { error ->
logger.e("WebAuthn authentication failed", error)
}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)
}
})callback.bind {
this.deviceName = deviceName
// Optional configuration
}.onFailure {
logger.e("Device binding failed", it)
}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);
}
});
}deviceProfileCallback.collect {
collectors.apply(DefaultDeviceCollector())
}DeviceIdentifier.builder().context(applicationContext).build().identifierDefaultDeviceIdentifier.id()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)
}
}val callback = PingOneProtectInitializeCallback()
callback.start().onSuccess {
logger.i("PingOne Protect initialization successful")
}.onFailure { error ->
logger.e("PingOne Protect initialization failed", error)
}if (callback is PingOneProtectEvaluationCallback) {
callback.getData(context)
}val callback = PingOneProtectEvaluationCallback()
callback.collect().onSuccess {
logger.i("PingOne Protect evaluation successful")
}.onFailure { error ->
logger.e("PingOne Protect evaluation failed", error)
}val callback = ReCaptchaEnterpriseCallback()
callback.execute(application = application)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)
}// 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)
}
}// 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)
}
}// 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)
}
})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
}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)
}
})oidcWeb.authorize {
// Additional configuration
}.onSuccess { user ->
logger.i("Browser login successful")
}
.onFailure { error ->
logger.e("Browser login failed", error)
}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)
}
})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}")
}
}
}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 {
// 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)
}[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" }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. |