Ping SDKs

DaVinci tutorial for Android

This tutorial walks you through updating a provided sample app so that it connects to a PingOne tenant to authenticate a user using the PingOne sign-on with sessions DaVinci flow.

This flow allows users to register, authenticate, and verify their email address with PingOne. It combines the PingOne Sign On and Password Reset flow, which allows users to reset or recover their passwords, with the PingOne Registration and Email Verification flow, which allows users to register and verify their email. In addition, this flow checks for an active user session before prompting for authentication.

Features of the PingOne sign-on with sessions flow
Figure 1. Overview of the PingOne sign-on with sessions flow

You will implement code that:

  • Configures the app with the connection settings for your PingOne instance.

  • Starts a DaVinci flow

  • Renders UI depending on the type of node encountered in the flow

  • Returns responses to each node encountered

  • Gets an access token for the user on completion of the flow

  • Gets the user’s details from PingOne, such as their full name and email address

  • Logs the user out of PingOne

Before you begin

To successfully complete this tutorial refer to the prerequisites and compatibility requirements in this section.

The tutorial also requires a configured server.

Compatibility

PingOne
Android

The sample app is compatible with Android API levels 24 (Android 7) to 35 (Android 15)

Prerequisites

Android Studio

You can download the latest version for free from https://developer.android.com/studio/.

Server configuration

Task 1. Create an OAuth 2.0 application in PingOne

To register a public OAuth 2.0 client application in PingOne for use with the ForgeRock SDKs for Android and iOS, follow these steps:

  1. Log in to your PingOne administration console.

  2. In the left panel, navigate to Applications > Applications.

  3. Next to the Applications label, click the plus icon ().

    PingOne displays the Add Application panel.

  4. In Application Name, enter a name for the profile, for example sdkNativeClient

  5. Select Native as the Application Type, and then click Save.

  6. On the Configuration tab, click the pencil icon ().

    1. In Grant Type, select the following values:

      Authorization Code

      Refresh Token

    2. In Redirect URIs, enter the following value:

      org.forgerock.demo://oauth2redirect

    3. In Token Endpoint Authentication Method, select None.

    4. Click Save.

  7. On the Resources tab, next to Allowed Scopes, click the pencil icon ().

    1. In Scopes, select the following values:

      email

      phone

      profile

      SDK Revoke Resource

      The openid scope is selected by default.

      The result resembles the following:

      Ensure you select the custom `revoke` scope.
      Figure 2. Adding scopes, including the custom "revoke" scope to an application.
  8. On the Policies tab, click the pencil icon () to configure the DaVinci authentication policies for the application. PingOne policies are not supported.

    Applications that have no authentication policy assignments use the environment’s default authentication policy to authenticate users.

    1. To clear all PingOne policies, click Deselect all PingOne Policies.

    2. In the confirmation message, click Continue.

    3. On the DaVinci Policies tab, select the compatible policies that you want to apply to the application.

      The SDK currently is only compatible with the PingOne - Sign On and Registration flow and its subflows.

    4. Click Save.

      PingOne applies the first policy in the list.

  9. Click Save.

  10. Enable the OAuth 2.0 client application by using the toggle next to its name:

    Enable the application using the toggle.
    Figure 3. Enable the application using the toggle.

The application is now configured to accept client connections from and issue OAuth 2.0 tokens to native mobile applications.

Task 2. Get configuration values from PingOne

You will need some of the property values from the client profile you created to configure the SDKs:

  1. Log in to your PingOne administration console.

  2. In the left panel, click Applications.

  3. Select the OAuth 2.0 application profile you created for the application.

  4. On the Configuration tab, expand the URLs section, and make a note of the following values:

    OIDC Discovery Endpoint

    Client ID

    Obtaining values from the OIDC application profile in PingOne.
    Figure 4. Obtaining values from the OIDC application profile in PingOne.

Step 1. Downloading the sample app

You need to download source code from the SDK sample apps repo.

  1. In a web browser, navigate to the SDK sample apps repository.

  2. Download the source code using one of the following methods:

    Download a ZIP file
    1. Click Code, and then click Download ZIP.

    2. Extract the contents of the downloaded ZIP file to a suitable location.

    Use a Git-compatible tool to clone the repo locally
    1. Click Code, and then copy the HTTPS URL.

    2. Use the URL to clone the repository to a suitable location.

      For example, from the command-line you could run:

      git clone https://github.com/ForgeRock/sdk-sample-apps.git.git

The result of these steps is a local folder named sdk-sample-apps.

Step 2. Preparing the sample project

In this section you open the sample project in Android Studio, and view the integration points in the TODO pane.

  1. In Android Studio, click Open, navigate to the sdk-sample-apps folder that has the downloaded sample source code in, and then click Open.

    Android Studio opens and loads the DaVinci tutorial project.

  2. In the Project pane, navigate to samples > app.

  3. On the View menu, select Tool Windows, and then click TODO.

    The sample code is annotated with TODO comments to help you locate the integration points, where code changes are required.

    android sample todo pane en
    Figure 5. Integration points annotated with TODO comments

Step 3. Adding code to the sample

In this section you visit each integration point in the sample app to understand how to complete a DaVinci flow, including handling the different nodes and their collectors, obtaining an access token and user information, and finally signing out of the session.

  1. In the TODO pane, double-click the STEP 1 line.

    Android Studio opens DaVinciViewModel.kt:

    DaVinciViewModel.kt
    //TODO: Integration Point. STEP 1
    val daVinci = DaVinci {
        logger = Logger.STANDARD
    
        // Oidc as module
        module(Oidc) {
            clientId = "<Client ID>"
            discoveryEndpoint = "<Discovery Endpoint>"
            scopes = mutableSetOf("<scope1>", "<scope2>", "…​")
            redirectUri = "<Redirect URI>"
        }
    }

    This snippet initializes the DaVinci module, and leverages the OpenID Connect (OIDC) module to configure the settings to connect to your PingOne instance.

    1. Replace <Client ID> with the ID of the client you are connecting to in PingOne.

      Example:

      clientId = "6c7eb89a-66e9-ab12-cd34-eeaf795650b2"

      Refer to Task 2. Get configuration values from PingOne for instructions of where to find this value.

    2. Replace <Discovery Endpoint> with the OIDC Discovery Endpoint value from the client you are connecting to in PingOne.

      Example:

      discoveryEndpoint = "https://auth.pingone.ca/3072206d-c6ce-ch15-m0nd-f87e972c7cc3/as/.well-known/openid-configuration"

      Refer to Task 2. Get configuration values from PingOne for instructions of where to find this value.

    3. In the scopes property, add the scopes you want to assign users who complete authentication using the client.

      Example:

      scopes = mutableSetOf("openid", "email", "profile")

    4. Replace <Redirect URI> with the application ID of your sample app, followed by ://oauth2redirect.

      Example:

      redirectUri = "org.forgerock.demo://oauth2redirect"

      The redirectUri value you use must exactly match one of the Redirect URIs value you enter in the native OAuth 2.0 application you created earlier.

    5. Optionally, delete the TODO comment to remove it from the list.

    The result resembles the following:

    DaVinciViewModel.kt
    val daVinci = DaVinci {
        logger = Logger.STANDARD
    
        // Oidc as module
        module(Oidc) {
            clientId = "6c7eb89a-66e9-ab12-cd34-eeaf795650b2"
            discoveryEndpoint = "https://auth.pingone.ca/3072206d-c6ce-ch15-m0nd-f87e972c7cc3/as/.well-known/openid-configuration"
            scopes = mutableSetOf("openid", "email", "profile")
            redirectUri = "org.forgerock.demo://oauth2redirect"
        }
    }
  2. In the TODO pane, double-click the STEP 2 line.

    Android Studio opens DaVinciViewModel.kt:

    DaVinciViewModel.kt
    //TODO: Integration Point. STEP 2
    // Start the DaVinci flow, next node from the flow will be returned
    // Update the state with the next node
    
    /*
    val next = daVinci.start()
    
    state.update {
        it.copy(prev = next, node = next)
    }
    */

    This snippet calls start() to start the DaVinci flow, and assigns the returned node to the variable next.

    It also updates the app’s state to store the response as both prev and node.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

  3. In the TODO pane, double-click the STEP 3 line.

    Android Studio opens DaVinciViewModel.kt:

    DaVinciViewModel.kt
    //TODO: Integration Point. STEP 3
    // Continue the DaVinci flow, next node from the flow will be returned
    // Update the state with the next node
    
    /*
    val next = current.next()
    
    state.update {
        it.copy(prev = current, node = next)
    }
    */

    This snippet calls next() to continue the DaVinci flow, by proceeding to the next available node. It assigns the newly returned node to the variable next.

    It also updates the app’s state to store the new response as node, and the current node as prev.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

  4. In the TODO pane, double-click the STEP 4 line.

    Android Studio opens DaVinci.kt:

    DaVinci.kt
    //TODO: Integration Point. STEP 4
    // Render the current node depending on its type
    
    /*
    when (val node = state.node) {
        is ContinueNode → {
            Render(node = node, onNodeUpdated, onStart) { onNext(node) }
        }
        is FailureNode → {
            Log.e("DaVinci", node.cause.message, node.cause)
            Render(node = node)
        }
        is ErrorNode → {
            Render(node)
            // Render the previous node
            if (state.prev is ContinueNode) {
                Render(node = state.prev, onNodeUpdated, onStart) { onNext(state.prev) }
            }
        }
        is SuccessNode → {
            LaunchedEffect(true) { onSuccess?.let { onSuccess() } }
        }
        else → {}
    }
    */

    This snippet watches for a change in state, and takes an action based on the ode type returned by DaVinci.

    For example, if it is a FailureNode log an error message. If the node is a ContinueNode that continues the flow, call the render function to display the necessary fields on the screen.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

  5. In the TODO pane, double-click the STEP 5 line.

    Android Studio opens ContinueNode.kt:

    ContinueNode.kt
    //TODO: Integration Point. STEP 5
    // Intermediate step in the Davinci Flow. The ContinueNode is a node that is used to
    // continue the flow. It can have multiple collectors that are used to collect user input.
    // Render the UI for each collector that are part of the ContinueNode.
    
    /*
    continueNode.collectors.forEach {
        when (it) {
            is FlowCollector → {
                hasAction = true
                FlowButton(it, onNext)
            }
    
            is PasswordCollector → {
                Password(it, onNodeUpdated)
            }
            is SubmitCollector → {
                hasAction = true
                SubmitButton(it, onNext)
            }
    
            is TextCollector → Text(it, onNodeUpdated)
        }
    }
    */

    This snippet handles the various collectors that are returned by the current node.

    Loop through all of the collectors in the node and render an appropriate field in your app.

    For example, this snippet handles text and password fields, and two types of button, a submit and a flow type.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

  6. In the TODO pane, double-click the STEP 6 line.

    Android Studio opens TokenViewModel.kt:

    TokenViewModel.kt
    //TODO: Integration Point. STEP 6
    // Retrieve the access token
    
    /*
    User.user()?.let {
       when (val result = it.token()) {
            is Failure → {
                state.update {
                    it.copy(token = null, error = result.value)
                }
            }
    
            is Success → {
                state.update {
                    it.copy(token = result.value, error = null)
                }
            }
        }
    } ?: run {
        state.update {
            it.copy(token = null, error = null)
        }
    }
    */

    This snippet gets called when the flow reaches a SuccessNode and gets an access token on behalf of the authenticated user.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

  7. In the TODO pane, double-click the STEP 7 line.

    Android Studio opens UserProfileViewModel.kt:

    UserProfileViewModel.kt
    //TODO: Integration Point. STEP 7
    // Retrieve the user info
    
    /*
    User.user()?.let { user →
        when (val result = user.userinfo(false)) {
            is Result.Failure →
                state.update { s →
                    s.copy(user = null, error = result.value)
                }
    
            is Result.Success →
                state.update { s →
                    s.copy(user = result.value, error = null)
                }
        }
    }
    */

    This snippet gets the user’s info from DaVinci, for example their preferred name for display within the sample app.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

  8. In the TODO pane, double-click the STEP 8 line.

    Android Studio opens LogoutViewModel.kt:

    LogoutViewModel.kt
    //TODO: Integration Point. STEP 8
    // logout the user
    
    /*
    User.user()?.logout()
    */

    This snippet calls the logout() function on the User object to sign the user out of the app, and end their session in DaVinci.

    1. Uncomment the highlighted text.

    2. Optionally, delete the TODO comment to remove it from the list.

You are now ready to run the sample app on a real or simulated Android device.

Step 4. Test the sample app

After updating the code, you can now run the sample app on a connected or simulated Android device.

  1. Add or connect a device to Android Studio.

    Learn more about devices in Android Studio in the Android Developer documentation:

  2. On the Run menu, click Run 'samples.app'.

    Android Studio starts the sample application on the simulated or connected device.

    The app automatically starts the DaVinci flow:

    The DaVinci sample app first screen with fields and buttons.
    Figure 6. The DaVinci sample app first screen with fields and buttons.
  3. Optionally, to register a new identity in PingOne, tap the No Account? Register now! link.

    This link is an example of a FlowButton.

    The app displays the registration screen:

    The DaVinci sample app registration screen. image::sdks:davinci:davinci-sample-register-en.png[alt="The DaVinci sample app registration screen", width=350]

    1. Enter the details of the new identity, and then click Save.

      The app creates the new identity in PingOne and returns to the sign on screen.

  4. Enter the username and password of a PingOne identity, and then click Sign On.

    The app sends the credentials to PingOne for validation, and on success displays the user’s info:

    The DaVinci sample app displaying user info
    Figure 7. The DaVinci sample app displaying user info
  5. Tap the menu () icon, and then tap generating_tokens Show Token.

    The app shows the access token obtained on behalf of the user.

    The DaVinci sample app displaying a user’s access token
    Figure 8. The DaVinci sample app displaying a user’s access token
  6. Tap the menu () icon, and then tap logout Logout.

    The app revokes the existing tokens and ends the session in PingOne.

Troubleshooting

This section contains help if you encounter an issue running the sample code.

What can cause validation errors in the request?

When starting the app you might see an error message as follows:

The request could not be completed. One or more validation errors were in the request.

davinci sample validation error en
Figure 9. Validation error when starting the app

The Logcat pane in Android Studio often contains additional information about the error:

{
  "id" : "2a72bf00-5f20-4b78-a7d0-ad8d95e9b11b",
  "code" : "INVALID_DATA",
  "message" : "The request could not be completed. One or more validation errors were in the request.",
  "details" : [
    {
      "code" : "INVALID_VALUE",
      "target" : "redirect_uri",
      "message" : "Redirect URI mismatch"
    }
  ]
}

In this case the cause is Redirect URI mismatch.

Ensure that your redirectUri value in com/pingidentity/samples/app/davinci/DaVinciViewModel.kt exactly matches one of the values you entered in the Redirect URIs field in your OAuth 2.0 application in PingOne:

.Match the redirect URIs in the sample app and PingOne configuration
Figure 10. Match the redirect URIs in the sample app and PingOne configuration