Skip to content

Latest commit

 

History

History
682 lines (595 loc) · 25.2 KB

File metadata and controls

682 lines (595 loc) · 25.2 KB

Journey Module Migration: Forgerock to Ping SDK

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 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.