Skip to content

Latest commit

 

History

History
473 lines (387 loc) · 20.1 KB

README.md

File metadata and controls

473 lines (387 loc) · 20.1 KB

Ping Identity

Journey Module

Journey is an Android library designed to streamline Authentication and Authorization processes for applications utilizing Ping Advanced Identity Cloud and PingAM.

Getting Started

Prerequisites

Installation

To integrate this module into your Android project, include the following dependency in your build.gradle.kts (or build.gradle) file:

dependencies {
    implementation("com.pingidentity.sdks:journey:<version>")
}

Replace <version> with the latest available version of the SDK from the Maven repository. Ensure your project's repositories block includes Maven Central or the Ping Identity Maven repository.

Overview

Journey offers a straightforward API to manage authentication flows and handle the various states encountered during this process.

sequenceDiagram
    Developer ->> Journey: Create Journey instance
    Journey ->> Developer: Journey
    Developer ->> Journey: start(Journey Name)
    Journey ->> AIC: /authenticate
    AIC ->> AIC: Launch Journey Flow
    AIC ->> Journey: Callback(s)
    Journey ->> Developer: Node
    Developer ->> Developer: Gather credentials
    Developer ->> Journey: next()
    Journey ->> AIC: /authenticate
    AIC ->> Journey: Session Token
    Journey ->> Developer: Session Token
    opt OIDC Module
        Journey ->> AIC: /authorize
        AIC ->> Journey: Authorization Code
        Journey ->> AIC: /token
        AIC ->> Journey: Access Token
        Journey ->> Journey: persist access token
        Journey ->> Developer: Access Token
    end
Loading

For a deeper understanding of PingOne AIC Journeys, refer to the official documentation available here.

Basic Usage

To begin using the Journey class, instantiate it within your code, providing a configuration block to customize its behavior. This configuration allows you to set parameters such as network timeout and logging preferences.

Here's a basic example of creating a Journey instance and initiating the authentication flow:

val journey = Journey {
    serverUrl = "https://openam-sdks.forgeblocks.com/am"
}
var node = journey.start("login") // Start the login authentication flow
node = node.next()

Integrating the OIDC Module

Optionally, you can enhance the Journey instance with the oidc module. This module enables the discovery of OpenID Connect (OIDC) endpoints using the discoveryEndpoint attribute.

val journey = Journey { 
    serverUrl = "your_server_url"
    module(Oidc) {
        clientId = "your_client_id"
        discoveryEndpoint =
            "https://your_openam_domain/am/oauth2/alpha/.well-known/openid-configuration"
        scopes = mutableSetOf("openid", "email", "address", "profile", "phone")
        redirectUri = "org.forgerock.demo:/oauth2redirect"
        // Add other OIDC configurations as needed
    }
}

Advanced Journey Configuration

The Journey configuration block offers further customization options:

val journey = Journey {
    timeout = 30000 // Network request timeout in milliseconds (default: 15000ms)
    logger = Logger.STANDARD // Use the standard logger for Logcat output
    serverUrl = "<server_url>" // Specify the server URL
    realm = "<realm_name>" // Specify the realm for authentication
    cookie = "<cookie_name>" // Specify the cookie name for session management
    module(Oidc) {
        clientId = "your_client_id"
        discoveryEndpoint =
            "https://your_openam_domain/am/oauth2/alpha/.well-known/openid-configuration"
        // ... other OIDC configurations

        // Storage configuration options
        storage {
            fileName = "test"
            keyAlias = "myKeyAlias"
            strongBoxPreferred = true
            cacheStrategy = CacheStrategy.CACHE_ON_FAILURE
        }
    }
    module(Session) {
        storage {
            //keep default filename and keyalias.
            strongBoxPreferred = false
            cacheStrategy = CacheStrategy.CACHE_ON_FAILURE
        }
    }
}

Storage Configuration Explained

The storage block allows you to configure how data is persisted for each module. The options include:

  • fileName: The name of the file used for persistent storage.
  • keyAlias: If provided, enables encryption using AndroidKeyStore. This results in the use of EncryptedDataStoreStorage.
  • strongBoxPreferred: If set to true, attempts to use hardware-backed StrongBox for key storage (if available).
  • cacheStrategy: Controls in-memory caching behavior. Options:
    • NO_CACHE: No caching, always fetch fresh data.
    • CACHE_ON_FAILURE: Cache in memory only if storage operation fails.
    • CACHE: Cache in memory, even if the storage operation fails.

Note: Data that store in the cache is kept in plain text and is not encrypted. A device that can output a memory dump may expose sensitive information

Navigating the Authentication Flow

The start() method initiates the authentication journey and returns a Node instance, representing the current state of the process. You can then use the next() method (available on ContinueNode) to transition to the subsequent state.

The resume(uri) method enables the continuation of an authentication flow after you've obtained a resume URI, for instance, through an Email Suspend Node.

Optionally, you can provide options when starting the journey, such as forceAuth and noSession

var node = journey.start("myLogin") {
    forceAuth = true
    noSession = true
} // Initiate the authentication journey

// 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 (e.g., 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 (e.g., network issues, unexpected errors like parsing response)
        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
    }
}

The Node class represents various states within the authentication journey:

Node Type Description
ContinueNode Indicates a step in the middle of the authentication journey. Call node.next() to advance to the next node.
ErrorNode Represents a bad request from the server, such as invalid credentials (password, OTP, username). Access the error message using node.message.
FailureNode Signifies an unexpected error during the process, like network connectivity issues. Retrieve the underlying cause using node.cause.
SuccessNode Indicates successful authentication. Obtain the user session details using node.session.

Providing User Input

When the current Node is a ContinueNode, it often contains a list of callbacks that require user input. You can access these callbacks using node.callbacks() and provide the necessary information to each relevant callback.

The following Callback will be supported in the Core Journey Module:

Callback Name Callback Description
BooleanAttributeInputCallback Collects true or false.
ChoiceCallback Collects single user input from available choices, retrieves selected choice from user interaction.
ConfirmationCallback Retrieve a selected option from a list of options.
ConsentMappingCallback Prompts the user to consent to share their profile data.
HiddenValueCallback Returns form values that are not visually rendered to the end user.
KbaCreateCallback Collects knowledge-based answers. For example, the name of your first pet.
MetadataCallback Injects key-value metadata into the authentication process.
NameCallback Collects a username.
NumberAttributeInputCallback Collects a number.
PasswordCallback Collects a password or one-time pass code.
PollingWaitCallback Instructs the client to wait for the given period and resubmit the request.
StringAttributeInputCallback Collects the values of attributes for use elsewhere in a tree.
SuspendedTextOutputCallback Pause and resume authentication, sometimes known as "magic links".
TextInputCallback Collects text input from the end user. For example, a nickname for their account.
TextOutputCallback Provides a message to be displayed to a user with a given message type.
TermsAndConditionsCallback Collects a user’s acceptance of the configured Terms & Conditions.
ValidatedCreatePasswordCallback Collects a password value with optional password policy validation.
ValidatedCreateUsernameCallback Collects a username value with optional username policy validation.

Here's how to access and populate the callbacks:

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

// Proceed to the next Node with the provided input
val nextNode = node.next()

Each specific callback type provides its own properties for accessing labels and setting values.

NameCallback

val prompt = (callback as? NameCallback)?.prompt // Access the prompt/label
(callback as? NameCallback)?.name = "Your Input" // Set the user's input

PasswordCallback

PasswordCallback offers similar properties to NameCallback:

val prompt = (callback as? PasswordCallback)?.prompt // Access the prompt/label
(callback as? PasswordCallback)?.password = "Your Secret" // Set the user's password

Handling Errors

The Journey SDK distinguishes between FailureNode and ErrorNode for different types of errors encountered during the authentication flow.

A FailureNode indicates an unexpected issue that prevents the journey from continuing. This could stem from network problems, data parsing errors, or internal SDK issues. In such cases, you can access the underlying Throwable that caused the error using node.cause(). It's generally recommended to display a user-friendly generic error message and log the details for support investigation.

An ErrorNode, on the other hand, signifies an error response from the authentication server ( typically an HTTP 4xx or 5xx status code). These errors often relate to invalid user input or issues on the server side. You can retrieve the specific error message provided by the server using node.message() and access the raw JSON response via node.input.

val node = journey.start() // Initiate the authentication flow

when (node) {
    is ContinueNode -> { /* ... */
    }
    is ErrorNode -> {
        val errorMessage = node.message // Retrieve the server-provided error message
        val rawResponse = node.input // Access the raw JSON error response
        // Display the specific error message to the user
    }
    is FailureNode -> {
        val errorCause = node.cause // Retrieve the underlying error
        // Log the errorCause for debugging
        // Display a generic error message to the user
    }
    is SuccessNode -> { /* ... */
    }
}

Integration with Jetpack Compose

For applications using Jetpack Compose, you can seamlessly integrate the Journey SDK using a ViewModel to manage the UI state.

ViewModel

class AuthViewModel : ViewModel() {
    private val _state = MutableStateFlow<Node>(Empty)
    val state: StateFlow<Node> = _state

    private lateinit var journey: Journey // Initialize your Journey instance

    fun initializeJourney() {
        journey = Journey {
            // Configure your Journey instance here
            serverUrl = "YOUR_SERVER_URL"
            // ... other configurations
        }
    }

    fun startJourney(flowName: String = "login") {
        viewModelScope.launch {
            val nextNode = journey.start(flowName)
            _state.update { nextNode }
        }
    }

    fun next(node: ContinueNode, input: Map<String, String> = emptyMap()) {
        viewModelScope.launch {
            val nextNode = node.next(input)
            _state.update { nextNode }
        }
    }
}

View (Composable)

import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun AuthScreen() {
    val viewModel: AuthViewModel = viewModel()
    val authState by viewModel.state.collectAsState()

    // Initialize Journey when the composable is created
    LaunchedEffect(key1 = Unit) {
        viewModel.initializeJourney()
        viewModel.startJourney()
    }

    when (val node = authState) {
        is ContinueNode -> {
            // Render UI elements based on the callbacks in the node
            // Example: Display text fields for NameCallback and PasswordCallback
            val callbacks = node.callbacks
            // ... UI logic to collect user input and call viewModel.handleCallbacks()
        }
        is ErrorNode -> {
            // Display the error message
            Text(text = "Authentication Error: ${node.message}")
        }
        is FailureNode -> {
            // Display a generic error message
            Text(text = "An unexpected error occurred.")
            // Optionally log the errorCause
            Log.e("AuthScreen", "Authentication Failure", node.cause)
        }
        is SuccessNode -> {
            // Navigate to the main application screen
            Text(text = "Authentication Successful!")
            // ... Navigation logic
        }
    }
}

Post Authentication Operations

After successful authentication using the Oidc module, the user's session information is typically stored securely. The Journey instance provides methods to interact with this stored session.

// Retrieve the existing user session. If an active session cookie (ST) exists, 'user' will not be null.
// Note: Even if a user object is retrieved, the access and refresh tokens within might be expired.
val user: User? = journey.user()
val session: SSOToken? = user?.ssoToken() // Retrieve the SSO token if available

user?.let {
    val accessToken = it.accessToken() // Retrieve the current access token
    it.revoke() // Revoke the current access and refresh tokens
    it.userinfo() // Fetch user information using the access token (if valid)
    it.logout() // Initiate the logout process, potentially clearing local session data
}

Journey Module Dependencies

The Journey module has several dependencies that are essential for its functionality.

C4Context
    title Journey Component Diagram
    Component(journey, "Journey", "Library", "Main Journey Library")
    Component(logger, "Logger", "Library", "Logging Utility")
    Component(journey_plugin, "Journey Plugin", "Library", "Plugin Support")
    Component(orchestrate, "Orchestrate", "Library", "Orchestration Logic")
    Component(storage, "Storage", "Library", "Data Storage")
    Component(android, "Android", "Library", "Android Specific Functionality")
    Component(utils, "Utils", "Library", "Utility Functions")
    Rel(journey, logger, "Depends on")
    Rel(journey, journey_plugin, "Depends on")
    Rel(journey, orchestrate, "Depends on")
    Rel(journey, storage, "Depends on")
    Rel(journey, android, "Depends on")
    Rel(journey, utils, "Depends on")

Loading

Journey's Callback Customization & Extension

Please refer to journey-plugin for customizing the Journey's callback.

Callback below will be supported by other modules:

Callback Name Callback Description
AppIntegrity Collects a generated token from the client to verify the integrity of the app
DeviceBinding Cryptographically bind a mobile device to a user account.
DeviceProfileCallback Collects meta and/or location data about the authenticating device.
DeviceSigningVerifier Verify ownership of a bound device by signing a challenge.
PingOneProtectEvaluationCallback Collects captured contextual data from the client to perform risk evaluations.
PingOneProtectInitializeCallback Instructs the client to start capturing contextual data for risk evaluations
ReCaptchaCallback Provides data required to use a CAPTCHA in your apps.
ReCaptchaEnterpriseCallback Provides data required to use reCAPTCHA Enterprise in your apps.
WebAuthnRegistrationCallback WebAuthn Registration.
WebAuthnAuthenticationCallback WebAuthn Authentication.
SelectIdpCallback External Identity provider selection.
IdpCallback External Identity provider authentication.

License

This software may be modified and distributed under the terms of the MIT license. See the LICENSE file for details.

© Copyright 2025-2026 Ping Identity Corporation. All rights reserved.