Migration From Ping Identity

Overview

This document will help you migrate your users from Ping Identity to FusionAuth.

There are a number of different ways applications can be integrated with Ping Identity, and it would be difficult to cover them all. This guide focuses on migrating user data, including profile data and passwords. However, Ping Identity does not allow for password hash export. Therefore, you must perform a slow migration if you don’t want to force users to reset their passwords.

Alternatively, you can do a bulk migration and force everyone to reset their passwords. This option is discussed below, but the primary focus of this guide is enabling you to migrate your users from Ping Identity without requiring any password resets.

Prerequisites

This guide assumes you already have FusionAuth installed. If not, please view our installation guides and install FusionAuth before you begin. For more general migration information, please view the general migration guide.

Planning Considerations

Slow Migration Or Bulk Migration

To preserve your users’ passwords, you need to perform a slow migration. Users log in to FusionAuth with their Ping Identity credentials, and FusionAuth transparently migrates their data. Slow migrations in FusionAuth use Connectors, a paid feature.

If, on the other hand, resetting user passwords is acceptable, a Bulk Migration can work for you. Review that section for more details on the required steps. You may also perform a bulk migration after a slow migration has run for a while. Active users can be transparently migrated and infrequent users may not mind resetting their password.

You can learn more about the types of migrations that FusionAuth supports here.

Mapping User Attributes

The attributes of the User object in FusionAuth are well documented.

If there is an attribute in your Ping Identity user which cannot be directly mapped to a FusionAuth attribute, you can place it in the user.data field. This field can store arbitrary JSON values and will be indexed and searchable.

One Tenant Or Many

The Ping Identity platform uses an organization-based model to define tenant accounts and their related entities. The organization is the top-level identifier. It defines your entire enterprise within the platform.

FusionAuth has the concept of Tenants. Both FusionAuth Tenants and Ping Identity organizations include data about users, applications, and other configuration.

Each tenant in FusionAuth is a distinct user space. You may choose to merge multiple Ping Identity organizations into one FusionAuth Tenant or keep them separate.

Learn more about FusionAuth Tenants.

Identity Providers

With Ping Identity, you can use the PingOne user directory or an external identity provider (IdP). Using an external IdP allows linked users to authenticate using the credentials provided by the external IdP. An external IdP includes mapping PingOne user attributes to attributes from the IdP. With FusionAuth, these are also called Identity Providers.

Review the supported FusionAuth Identity Providers to ensure any you need are supported. At this time, while there is considerable overlap between the supported identity providers, there are a number of differences.

If not supported explicitly, a provider may work with an OIDC or SAML connection. Otherwise, please open a feature request.

Importing Ping Identity users stored in an external identity provider is more straightforward because you can use the Import API; you don’t have to perform a slow migration. Many of the steps in Bulk Migration will apply.

To retrieve the user information, use the approach documented in the Export User Data From Ping Identity section.

Ping Identity also provides integrations with other social login providers such as Twitter, Google or Facebook. Review the supported FusionAuth Identity Providers to ensure your social providers are supported.

If not supported explicitly, a provider may work with an OIDC or SAML connection. Otherwise, please open a feature request.

When migrating social logins, you may need to modify the switches of the Ping Identity import script. See Use the Script for more.

Migrating users with social logins such as Apple or Facebook requires that you have an existing user Id for that provider. What this unique user Id looks like depends on the particular social identity provider. The unique Id may be an email address, an integer, UUID, or a random string.

Configure the appropriate FusionAuth Identity Provider with the same values (client_id, etc) as the original user management system you are migrating away from.

Import users with the Import API, assigning each user with a social login a random password such as a UUID.

Your next step depends on whether the social login provider’s unique identifier is available as part of your migration data. If you have the social login provider’s unique identifier, for each user, use the Link API to create a link with the appropriate User Id, Identity Provider Id and Identity Provider User Id.

  • The User Id is the Id of the recently created FusionAuth User.
  • The Identity Provider Id is found on the corresponding Identity Provider API documentation. Look for identityProvider.id .
  • The Identity Provider User Id is the existing social provider user identifier exported or otherwise extracted from the original system.

You do not need to migrate the social network token, which may or may not be accessible. During the first login of a newly migrated user, FusionAuth finds the unique user in the social login provider based on the migrated Identity Provider User Id, and completes the login. During this process, FusionAuth stores a token on the Link, if the social provider returns one. Depending on the configuration of the social provider, users may see a prompt asking if they want to allow FusionAuth to have access to user data such as email address.

IdP Linking Strategies are available since version 1.28.0. Before that version, users were linked on email.

If you do not have the social login provider’s identifier, you need to decide if you want to transparently link the two accounts, which is easier for the end user, or if you want to ask the user to manually link the accounts, which is more accurate, but may be confusing.

To transparently link the accounts, choose a linking strategy of Link On Email or Link On Username, which will create the user if they don’t exist. However, if the user has an email address at their social provider which differs from the email address that was used to sign up for your application and which you imported to FusionAuth, then two accounts will be created.

For example, if the user has a Google account richard@gmail.com, but signed up for your application with richard@fusionauth.io, then if you use the Link On Email strategy, two different accounts will be created, since FusionAuth is trying to match on email address and they don’t. The same holds true for usernames with the Link on Username strategy.

To prompt the user to link the accounts, choose a linking strategy of Pending, which will prompt the end user to sign into FusionAuth after they sign into the social provider, authoritatively linking the two accounts.

Here’s more information about IdP Linking Strategies.

Other Entities

There are often other important entities, such as app clients or password policies, that need to be migrated. There are usually fewer of these, so an automated migration may not make sense, but plan to move this configuration somehow.

Be aware that functionality may not be the same between Ping Identity and FusionAuth. This is different from user data; as long as you can somehow migrate a login identifier (a username or email) and a password hash, a user will be authenticated and successfully migrated. You can download FusionAuth before you begin a migration and build a proof of concept to learn more about the differences.

A partial list of what may need to be migrated for your application to work properly includes the following:

  • Ping Identity uses DaVinci, an orchestration platform that lets you create flows using connections and logical operators. These flows guide users through defined processes that can present customized pages, modify values, or perform other actions. In FusionAuth, Applications are a similar construct, but users are associated with them through Registrations.
  • Ping Identity uses Flows in DaVinci. A flow is a user journey, such as registration or authentication, built from a set of capabilities and logical operators. FusionAuth has a similar concept called Lambdas. FusionAuth also has webhooks fired at certain points in the user lifecycle; in certain configurations, they can also stop a particular authentication flow.
  • In Ping Identity a role is a collection of permissions that can be assigned to a user, application, or connection. FusionAuth has roles that are configured on an application-by-application basis and made available in a token after successful authentication.
  • Ping Identity allows using groups to organize a collection of user identities to manage access to applications. FusionAuth also has groups.
  • Ping Identity makes use of populations. A population defines a set of users and can help you make user management simple. In FusionAuth, you can manage a set of users via a Tenant.
  • Ping Identity sends emails on your behalf, such as forgotten password notifications. FusionAuth also sends emails, and the templates are customizable.
  • Ping Identity supports Client Credentials grant. FusionAuth offers a constrained version of this using Entity Management; this is available in all paid editions.
  • Ping Identity allows for custom attributes, but they must be configured at the Directory level. In FusionAuth, as mentioned above, custom user attributes are stored on the user.data field and are dynamic, searchable, and unlimited in size. Any valid JSON value may be stored in this field.
  • Ping Identity supports multi-factor authentication (MFA). FusionAuth also supports MFA, which may be enabled for a tenant and configured for a user at any time.

In FusionAuth, users are explicitly mapped to applications with a Registration. In Ping Identity:

  • Users get assigned to a directory in the environment as part of an organization.
  • These users then by default get access to the applications defined in this environment, but access might be dependent on certain roles.

Differences

In addition to the different names for common functionality outlined above in Other Entities , there are some fundamental differences between FusionAuth and Ping Identity. If your application relies on Ping Identity-specific functionality, please review this section carefully.

  • Ping Identity has certain pricing limitations, like number of users. FusionAuth has no user limit. Instead, you are limited by the resources provided to a FusionAuth instance, such as memory, CPU, and database capacity.
  • FusionAuth does not support custom scopes. There is an open feature request.

Once you’ve planned the migration of other entities, the next step is to set up FusionAuth to connect to Ping Identity to import users during login.

Login UI

Ping Identity allows custom branding and theming on the UI. Login pages are highly variable across Ping Identity instances. This is beyond the scope of this document.

The FusionAuth login experience follows two paths: You can choose to build your own login pages or use the FusionAuth-hosted login pages. Read more about these choices.

Importing Users

Because you are implementing a slow migration, it will take place over time. But you can set up a test environment to confirm it will work before deploying to production.

Here are the steps you need to take to import the user data.

  1. Set up FusionAuth.
  2. Set up Ping Identity to export users.
  3. Configure endpoints.
  4. Set up authentication logic.
  5. Set up the connector.
  6. Log in as a test user.
  7. Verify the import.
  8. Migrate everything else.

Set Up FusionAuth

You need to set up FusionAuth so migrated user data can be stored. As mentioned above, this guide assumes you have FusionAuth installed.

If you don’t, view our installation guides and install it before proceeding further.

Create A Test Tenant

It is best to create a separate tenant for migration testing. Tenants logically isolate configuration settings and users. If a migration goes awry or you need to redo it after tweaking settings, you can delete the test tenant and start with a clean system. To add a tenant, navigate to Tenants and choose the Add button (green plus sign).

Adding a tenant.

Give it a descriptive Name like Ping Identity import test. You shouldn’t need to modify any of the other configuration options to test importing users.

Save the tenant.

The tenant creation screen.

Record the Id of the tenant, which will be a UUID. It will look something like 25c9d123-8a79-4edd-9f76-8dd9c806b0f3. You’ll use this later.

The tenant list.

Create A Test Application

Applications are anything a user can log in to. In FusionAuth there’s no differentiation between web applications, SaaS applications, APIs and native apps. To add an application, navigate to Applications and click on the Add button (the green plus sign). Give the application a descriptive name like Ping Identity application.

Select your new tenant, created above, in the dropdown for the Tenant field.

Navigate to the OAuth tab and add an entry to Authorized redirect URLs . Use a dummy value such as https://example.com. Later, you’ll need to update this to be a valid redirect URL that can take the authorization code and exchange it for a token. Learn more about this in the FusionAuth OAuth documentation.

You shouldn’t need to modify any of the other configuration options to test importing users. Save the application.

The application creation screen.

Next, view the application by clicking the green magnifying glass and note the OAuth IdP login URL . You’ll be using it to test that users can log in.

Finding the login URL.

Set Up Ping Identity To Export Users

There are two main components you need to set up to enable a slow migration from Ping Identity.

  • A publicly available API endpoint that can be called from a FusionAuth Connector.
  • A Ping Identity authentication “stub” to retrieve the user’s details to be imported to FusionAuth.

Set Up An API Endpoint

You have a few options for setting up an API endpoint.

  • Using Microsoft Azure, you can create an HTTP trigger on an Azure function app.
  • You can use an API gateway on AWS.
  • You can create a self-hosted endpoint locally and make it public using a service like ngrok.

This guide will show you how to create an ngrok endpoint that will use a Flask API to validate user credentials from FusionAuth login requests and retrieve the user’s Ping Identity profile from the PingOne API.

ngrok can be downloaded at https://ngrok.com. There are free and paid plans available. This guide uses a free plan.

Follow this quickstart to get ngrok up and running.

When ngrok is installed, start it with this command.

ngrok http 5001

Take note of the URL ngrok prints to your terminal, as you will need it later. It should look something like https://random-ngrok-string.ngrok-free.app.

Set Up A Ping Identity Stub To Authenticate Users

There are several ways you can set up a Ping Identity authentication stub.

  • With Microsoft Azure, you can use a Microsoft Azure function app.
  • You can use an AWS Lambda function.
  • You can create a Flask app that exposes an endpoint used with ngrok and configured as a Connector on FusionAuth.

This guide will show you how to use Flask and ngrok.

The authentication stub accepts a login request from FusionAuth over TLS and performs an ROPC login request on Ping Identity, returning the relevant user data via the PingOne API.

Add The PingOne Authorize Service To Your Environment

To connect to the Ping Identity API, add the PingOne Authorize service to your environment and then create a worker application in the environment. To add the PingOne Authorize service to your environment, go to Overview → Services and click the + button. Click + Add next to PingOne Authorize service to add the service.

Add PingOne Authorize service to environment.

Add A Worker Application For The PingOne Authorize Service

After setting up the PingOne Authorize service in your environment, add a worker application to enable dynamic authorization of API actions for the service. Under Applications → Applications click the + button to create a new application. Set the Application Name and select the “Worker” application type. Save the application.

Add worker application to environment.

After saving, enable the application by clicking the toggle button (top right) next to the application name. Give the worker application sufficient permissions to access your user data by assigning the necessary roles, for example, the “Identity Data Admin” role for your environment.

Add roles to worker application.

Generate an access token in the Configuration tab by clicking the Get Access Token and take note of it to use in the Flask application you will create shortly.

Get access token.

On the Overview tab of the application take note of the Environment ID .

Add An OIDC Application In Your Ping Environment

Create an OIDC application in your Ping Environment to call from the Flask app. Under Applications → Applications click the + button to create a new application. Set the Application Name and select the “OIDC Web App” application type. Save the application. After saving, enable the application by clicking the toggle button (top right) next to the application name.

On the Configuration tab click the pencil edit button to edit the configuration details of the application.

  • Select “Code” as the Response Type .
  • Select “Authorization Code” as the Grant Type .
  • Set https://www.google.com for the Redirect URIs
  • Take note of the Client Id of the application to use in the Flask app and save the configuration.
OIDC application configuration

FusionAuth Connectors will send user credentials to Ping Identity via a Python Flask app for demonstration purposes. In production migrations, it is important to secure both the data in transit and the various endpoints that form part of the solution.

In this guide, all communication is over TLS, and the Python Flask app can use a unique query string code for security purposes. Regardless of the migration implementation you choose, security in production migrations should be the highest priority and proper review and planning regarding this should be done.

Ping Identity also uses an access token that needs certain administrative permissions to perform the functions needed. Please bear in mind that the access token may expire and need to be renewed.

Set Up Authentication Logic

This section relies on Python being available on your system. Python can be downloaded from https://www.python.org/downloads/.

Create a directory to set up the Flask application.

mkdir authenticator_app && cd authenticator_app

Set up a virtual environment in the authenticator_app directory by running the following command.

python -m venv venv

Activate the virtual environment with the command.

source venv/bin/activate

Install requirements for the Flask application in the virtual environment with the following command.

pip install requests flask

Create a pingIdentityAuth.py file and add the Flask authenticator code below to it. Modify the example function code provided as needed and remember to update the domains (for example, apiPath) to match the region where the Ping Identity data is hosted.

The following fields need to be populated from your environment:

  • envID: The Id of the environment the user data will be exported from.
  • appId: The Client Id of the OIDC application.
  • access_token: The access token you generated when you created the worker application in Ping Identity.
# python -m venv venv
# source venv/bin/activate
# pip install requests flask

import requests
import json
from flask import Flask, render_template,  jsonify, request

app = Flask(__name__)

authPath = 'https://auth.pingone.com' # change domain to the one for the region where your user data is stored
apiPath ='https://api.pingone.com/v1' # change domain to the one for the region where your user data is stored
envID = 'YOUR ENVIRONMENT ID'
appId = 'YOUR PING IDENTITY OIDC APP CLIENT ID' 
accessToken = "YOUR ACCESS TOKEN"


def sentAuthRequest(appId):
    url = f"{authPath}/{envID}/as/authorize?response_type=code&client_id={appId}&redirect_uri=https://www.google.com&scope=openid"
    payload = {}
    headers= {}
    response = requests.request("GET", url, headers=headers, data = payload, allow_redirects=False)

    if response.status_code!=302:
        print(response.status_code)
        print(response.text.encode('utf8'))
    else:
        return  response.headers['Location'].split("flowId=")[1]

def getFlowId(flowId):

    url = f"{authPath}/{envID}/flows/{flowId}"
    payload = {}
    headers = {}
    response = requests.request("GET", url, headers=headers, data = payload)
    if response.status_code!=200:
        print(response.status_code)
        print(response.text.encode('utf8'))
        return None
    else:
        return flowId

def submitCredentials(flowID,username,userPassword):
    url = f"{authPath}/{envID}/flows/{flowID}"
    payload = "{\n    \"username\": \""+ username +"\",\n    \"password\": \""+ userPassword+"\"\n}"
    headers = {
    'Content-Type': 'application/vnd.pingidentity.usernamePassword.check+json'
    }
    response = requests.request("POST", url, headers=headers, data = payload)

    if response.status_code!=200:
        print(response.status_code)
        print(response.text.encode('utf8'))
    else:
        result = transform_json_user(response.content)
        return result


def transform_json_user(input_json):
    dataJson = json.loads(input_json)
    userId = dataJson["_embedded"]['user']['id']
    dataJson =  readUserInfo(userId)
    transformed_user = {
        "active": dataJson.get("enabled", False),
        "birthDate": None,
        "data": {
            "migrated": True,
        },
        "email": dataJson.get("email", ""),
        "expiry": None,
        "firstName": dataJson["name"].get("given", ""),
        "fullName": "",  # user["name"]["formatted"],
        "id": dataJson.get("id", ""),
        "lastLoginInstant": 0,
        "lastName": dataJson["name"].get("family", ""),
        "middleName": "",
        "passwordChangeRequired": False,
        "passwordLastUpdateInstant": 0,
        "preferredLanguages": ["en"],
        "timezone": None,
        "username": dataJson.get("username", ""),
        "verified": dataJson.get("verifyStatus", False)==True,
   }

    result_json = {"user": transformed_user}
    return result_json

def readUserInfo(userId):
    url =f'{apiPath}/environments/{envID}/users/{userId}?expand=population'
    headers = {'Authorization': 'Bearer ' +accessToken }
    response = requests.get(url,headers=headers,timeout=30)

    if response.status_code!=200:
            print(response.status_code)
            print(response.text.encode('utf8'))
    else:
        jsonData = response.json()
        return jsonData


@app.route('/api/RopcProxy', methods=['POST'])
def rocp_proxy():
    # Extract the 'code' query parameter from the URL
    code = request.args.get('code')

    if not code:
        return jsonify({'error': 'Missing code parameter'}), 400

    # Extract the 'loginId' and 'password' from the JSON body
    data = request.get_json()

    if not data or 'loginId' not in data or 'password' not in data:
        return jsonify({'error': 'Invalid or missing JSON body'}), 400

    login_id = data['loginId']
    password = data['password']

    response_data = TestCredentialLogin(appId,login_id,password)
    # print(json.dumps(response_data,indent=3))
    return response_data

def TestCredentialLogin(appId,username,password):
    print("--> Send auth request")
    flowId =sentAuthRequest(appId)
    print(flowId)
    print("--> Get Flow")
    flowId2=getFlowId(flowId)
    print(flowId2)
    print("--> Validating Credentials")
    result = submitCredentials(flowId2,username,password)
    print("--> Done")
    return result


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001,debug=True)

This code receives a login request from FusionAuth, attempts to log the user in via ROPC, and then returns a FusionAuth user object on success.

Following successful authentication, the function calls the PingOne API users endpoint to get additional user attributes, which are then transformed into a FusionAuth-compatible format.

Now run the application.

python pingIdentityAuth.py

Testing The Deployed API

The transform_json_user function converts the JSON returned from Ping Identity to the format FusionAuth requires.

Modify the transform_json_user function to match the user properties you want to import. The exact implementation depends on the custom attributes and business logic you used in Ping Identity. You could, for example, give users certain FusionAuth roles, register them for more than one application, or add them to a previously created FusionAuth group.

Please note that the FusionAuth connector will require a URL like the following in its configuration: https://random-ngrok-string.ngrok-free.app/api/RopcProxy?code=s0ndU863xdbXsFO4dLZAJQXLzyTU789iaUJ43uLAtIkXm_AzFuFWP1zg==. This is the URL exposed by ngrok and the Flask API. The code value in this example is just an arbitrary value representing the security token value you noted earlier.

Test the deployed API with the command below. Remember to use the credentials for your own Ping Identity test user.

curl -X POST 'https://random-ngrok-string.ngrok-free.app/api/RopcProxy?code=s0ndU863xdbXsFO4dLZAJQXLzyTU789iaUJ43uLAtIkXm_AzFuFWP1zg=='  \
  -H 'Content-type: application/json' \
  -d '{
    "loginId": "Richard",
    "password": "password"
  }'

If the authorization header or account credentials are incorrect, you will receive a 404 HTTP status code containing a message in the body. You can view the status code by running curl with the -v switch.

Otherwise, the following will be returned.

Sample User Data Response from Ping Identity

  {
    "_links": {
    "self": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529"
  },
    "environment": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0"
  },
    "population": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/populations/fbfccbc2-f853-4539-a2f1-c4210d155c51"
  },
    "devices": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/devices"
  },
    "roleAssignments": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/roleAssignments"
  },
    "password": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/password"
  },
    "password.reset": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/password"
  },
    "password.set": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/password"
  },
    "password.check": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/password"
  },
    "password.recover": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/password"
  },
    "linkedAccounts": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/linkedAccounts"
  },
    "account.sendVerificationCode": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529"
  },
    "memberOfGroups": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/users/cffa5359-aa0a-4d4c-8a1c-4cc18a36a529/memberOfGroups"
  }
  },
    "_embedded": {
    "population": {
    "_links": {
    "self": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0/populations/fbfccbc2-f853-4539-a2f1-c4210d155c51"
  },
    "environment": {
    "href": "https://api.pingone.com/v1/environments/de6b5f05-a95b-4ada-85bb-d8892700d1a0"
  }
  },
    "id": "fbfccbc2-f853-4539-a2f1-c4210d155c51"
  }
  },
    "id": "cffa5359-aa0a-4d4c-8a1c-4cc18a36a529",
    "environment": {
    "id": "de6b5f05-a95b-4ada-85bb-d8892700d1a0"
  },
    "account": {
    "canAuthenticate": true,
    "status": "OK"
  },
    "createdAt": "2024-02-18T07:00:18.420Z",
    "email": "richard.hendricks@piedpiper.com",
    "enabled": true,
    "identityProvider": {
    "type": "PING_ONE"
  },
    "lastSignOn": {
    "at": "2024-02-18T19:17:47.061Z",
    "remoteIp": "105.214.52.47"
  },
    "lifecycle": {
    "status": "ACCOUNT_OK"
  },
    "mfaEnabled": false,
    "name": {
    "given": "Richard",
    "family": "Hendricks"
  },
    "population": {
    "id": "fbfccbc2-f853-4539-a2f1-c4210d155c51"
  },
    "updatedAt": "2024-02-18T19:17:47.086Z",
    "username": "Richard",
    "verifyStatus": "NOT_INITIATED"
  }

The transform_json_user function will transform the returned JSON into FusionAuth-compatible JSON, which looks something like the following.

Sample Successful Login JSON

{
  "user": {
    "active": true,
    "birthDate": null,
    "data": {
      "migrated": true
    },
    "email": "richard.hendricks@piedpiper.com",
    "expiry": null,
    "firstName": "Richard",
    "fullName": "",
    "id": "cffa5359-aa0a-4d4c-8a1c-4cc18a36a529",
    "lastLoginInstant": 0,
    "lastName": "Hendricks",
    "middleName": "",
    "passwordChangeRequired": false,
    "passwordLastUpdateInstant": 0,
    "preferredLanguages": [
    "en"
    ],
    "timezone": null,
    "username": "Richard",
    "verified": false
  }
}

Once you have updated the custom function logic, it’s time to configure the Connector in FusionAuth. If you are migrating from Ping Identity using the AWS or Microsoft Azure approach, you’ll now deploy the Lambda function or Microsoft Azure function app.

Set Up The Connector

FusionAuth Reactor logo

This feature is only available in paid plans. Please visit our pricing page to learn more.

Now you’ll set up a Connector to use the ngrok Flask API you created. Connectors are a feature limited to paid editions of FusionAuth, so ensure you have a valid Reactor license. Learn more about activating Reactor.

Log in to the FusionAuth administrative user interface.

Configure A Connector

Navigate to Settings -> Connectors, click the dropdown button, and select Add Generic connector button on the top right.

Navigate to Settings, Connectors, and click Add Generic connector.

Configure the Connector:

Configuring a Generic Connector.

Save the Connector.

Note that if you have problems with the Connector, you can enable Debug enabled to have errors or warnings logged in the FusionAuth system event log.

Configure The Tenant

Now you’ll configure the tenant to use the Connector.

Navigate to Tenants -> Ping Identity import tenant -> Edit -> Connectors.

Click the Add policy button to set up a new Connector policy.

Connector policies for this Tenant.

Set the Connector field value to the name of the Connector you created. Make sure that the Migrate user field is enabled. You can leave the Domains field with the value of *, which will apply this Connector to every user. Submit the policy.

Ensure the new Connector Policy is at the top of the list of policies. Use the arrow buttons to move the policy if necessary. With this configuration, all users are checked against this Connector the first time they are seen. If they log in, they’ll be migrated to the FusionAuth user database.

After configuration, the Connector entry form should look similar to this:

Ping Identity Connector policy added and in list

Save the changes.

Log In As A Test User

To test that users will be migrated, log in as a test user via the FusionAuth interface with a valid user from your Ping Identity instance.

You’ll need the OAuth IdP login URL you recorded when you set up the test application.

Finding the login URL.

Copy this URL and open it in a new incognito browser window (if you don’t use an incognito window, the admin user session will interfere with the test). You should see the login screen:

The login page.

Enter credentials for a Ping Identity user account; you can use the same account you used to test the API with curl and log in. The user will be transparently migrated over to FusionAuth.

If the user was not migrated or the login was unsuccessful, enable Debug enabled in the Connector configuration to troubleshoot. Navigate to Settings -> Connectors and edit the Connector.

Try to log in again with the test user, then return to the FusionAuth UI and navigate to System -> Event Log to see if it contains any useful information.

After a successful test login, the user will be redirected to a URL like https://example.com/?code=2aUqU0ZhQCjtz0fnrFL_i7wxhIAh7cTfxAXEIpJE-5w&locale=en&userState=AuthenticatedNotRegistered.

This occurs because you haven’t set up a web application to handle the authorization code redirect yet. This is an important next step, but it’s beyond the scope of this document. Consult the 5-minute setup guide for an example of how to do this.

Verify The Import

You can also check that the import succeeded by viewing the user in the administrative user interface.

Migrated user in FusionAuth.

Once you have successfully migrated a user from Ping Identity to FusionAuth, any further profile or password changes to the user will occur against the FusionAuth database.

Clean Up Your Test Environment

When you have completed testing the migration, you can deploy the same configuration changes to production.

Depending on your architecture, you can choose to migrate users into the default tenant or a new tenant of the production instance. Whichever you choose, configure the Connector policy of the destination tenant.

If you aren’t keeping users in the test tenant, delete it. This is also useful if you want to start over because you need to tweak a setting such as the default application registration. In either case, delete the test tenant you created.

Deleting a tenant will remove all the users and other configuration for the tenant, giving you a fresh start. To delete a tenant, navigate to Tenants and choose the red trash can icon corresponding to the tenant to be deleted.

Deleting a tenant.

Confirm that you want to delete the tenant. Depending on how many users exist in a tenant, deleting a tenant may take some time. If it is easier, you may also delete migrated users one at a time using the administrative user interface.

Migrate Everything Else

You now have your users migrated, or a plan to do so. Congratulations! What is next?

You need to migrate additional configurations, as mentioned in Other Entities . Since the type of configuration varies, it is hard to provide a full list of how to import these items, but the general pattern will be:

  • Identify corresponding FusionAuth functionality.
  • Configure it in your FusionAuth instance, either manually or by scripting it using the client libraries or API.
  • Update your application configuration to use the new FusionAuth functionality.

Make sure you assign your users to the appropriate FusionAuth applications. You can do this either:

  • As part of your import process by adding registrations at import time.
  • After users have been migrated with the Registrations API.

You’ll also need to modify and test each of your applications, whether custom, open source, or commercial, to ensure:

  • Users can successfully log in.
  • The authorization code redirect is handled correctly.
  • Users receive appropriate permissions and roles based on the JWT.
  • The look and feel of the hosted login pages matches each application’s look and feel.

If your application uses a standard OAuth, SAML or OIDC library to communicate with , the transition should be relatively painless.

Estimate The Slow Migration Timeline

When using a slow migration, you can estimate how many accounts will be migrated in a given period.

You can calculate a timeline by knowing the following:

  • How frequently the average user logs in
  • What the distribution of your users’ login behavior is
  • What your migration goal is

Determining these values precisely is beyond the scope of this guide. However, a good rule of thumb is to determine how often your average user logs in to your application. Then you can use this formula to find out when a certain percentage of users have migrated: S = [(1 - (1 - P)^D)*100]%.

Breaking that equation down, you have:

  • P is the probability of any single user authenticating in a given day. So if your users log in once a week on a business day, it is 0.2 (one out of five days). If they log in once a year, it is 0.0027 (one out of 365 days).
  • D is the number of days of the migration period.
  • S is the percentage of users who have migrated.

For P = 0.2 and D = 10, S = 89%. Therefore, if your users log in one out of every five days, in ten days almost nine out of ten will be migrated to FusionAuth.

For P = 0.0027 and D = 370, S = 63.7%; if your users log in once a year on average, in about a year, almost two thirds of accounts will be migrated to FusionAuth.

Using the above calculations should help you estimate the duration of a migration.

After this period, you may want to bulk migrate the rest of the users, or treat them as inactive and not migrate them. Plan to disable the Connector and remove the tenant’s Connector Policy after the slow migration is complete.

Learn more about general slow migration considerations.

Bulk Migration

Be aware that the steps outlined for bulk migrating users from Ping Identity will not export password hashes and users will need to change their passwords manually on FusionAuth.

Export User Data From Ping Identity

Ping Identity provides a REST API to perform management functions. One of these endpoints allows you to read the user information in JSON format.

We recommend exporting your user information in JSON format so that you can import the data into FusionAuth using the FusionAuth API.

Creating The User File

To connect to the Ping Identity API and export users, create a worker application in your Ping Identity environment. Follow the steps in the Add The PingOne Authorize Service To Your Environment and Add A Worker Application For The PingOne Authorize Service sections.

Give the worker application sufficient permissions to access your user data by assigning the necessary roles, for example, the “Identity Data Admin” role for your environment.

Generate an access token in the Configuration tab and take note of it to use in the export application.

We’ve created a sample Python export program to export users from a Ping Identity server to a JSON file.

The following fields need to be populated from your environment:

  • envID: The Id of the environment the users will be exported from.
  • access_token: The access token generated when you created the worker application in Ping Identity.

Set up a Python virtual environment as described in the Set Up Authentication Logic section. Install the requests requirement for the Python script with pip install requests.

Clone the code below to a file named exportPingUsers.py and make modifications appropriate to your environment and needs. In addition to adding the envID and access_token, change the apiPath to point to the domain of the region where your Ping Identity data is hosted.

# python -m venv venv
# source venv/bin/activate
# pip install requests

import requests
from requests.auth import HTTPDigestAuth
import json

apiPath = 'https://api.pingone.com/v1' # change domain to the one for the region where your user data is stored

method_call = 'users'
envID = 'ADD YOUR ENVIRONMENT ID'
access_token = "ADD YOUR ACCESS TOKEN"
base_url = f'{apiPath}/environments/{envID}/'


# This method can be enhanced to add more or less data from the main export i.e. Custom Attributes
def transform_json(input_json):
    transformed_users = []

    for user in input_json["_embedded"]["users"]:
        transformed_user = {
            "active": user["enabled"],
            "birthDate": None,
            "insertInstance": None,  # Replace with appropriate value
            "data": {
                "migrated": True,  # Replace with appropriate value
                "favoriteColors": None,  # Replace with appropriate value
            },
            "email": user["email"],
            "expiry": None,
            "firstName":'',# ["name"]["family"],
            "fullName": '',#user["name"]["formatted"],
            "id": user["id"],
            "lastLoginInstant": 0,
            "lastName": user["name"]["family"],
            "middleName": "",
            "mobilePhone": "",
            "password": None,  # Replace with appropriate value
            "salt": None,  # Replace with appropriate value
            "factor": 10000,
            "encryptionScheme": "salted-pbkdf2-hmac-sha256",
            "passwordChangeRequired": False,
            "passwordLastUpdateInstant": 0,
            "preferredLanguages": ["en"],
            "identityProviders": {},
            "timezone": None,
            "twoFactorEnabled": False,
            "username": user["username"],
            "verified": True,  # Replace with appropriate value
        }

        transformed_users.append(transformed_user)

    result_json = {"users": transformed_users}
    return result_json


def main():
    url = base_url + method_call
    headers = {
        'Authorization': 'Bearer ' +access_token,
    }

    response = requests.get(
        url,
        headers=headers,
        timeout=30
    )

    print(response.status_code)

    result = response.json()
    with open("org.json", "w") as json_file:
        json.dump(result, json_file, indent=2)


    output = transform_json(result)

    with open("users.json", "w") as json_file:
        json.dump(output, json_file, indent=2)


if __name__ == "__main__":
    print("Exporting data....")
    main()
    print("Exporting done.")

Run the script with the following command.

python exportPingUsers.py

The data received from Ping Identity will look similar to the following.

{
    "_links": {
        "self": {
            "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users?limit=100"
        }
    },
    "_embedded": {
        "users": [
            {
                "_links": {
                    "password": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c/password"
                    },
                    "password.set": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c/password"
                    },
                    "account.sendVerificationCode": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c"
                    },
                    "linkedAccounts": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c/linkedAccounts"
                    },
                    "self": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c"
                    },
                    "password.check": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c/password"
                    },
                    "password.reset": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c/password"
                    },
                    "password.recover": {
                        "href": "https://api.pingone.com/v1/environments/24f17b5b-a458-4238-973e-0f77401897ed/users/916b9057-1de6-4718-931d-91f9e886316c/password"
                    }
                },
                "id": "916b9057-1de6-4718-931d-91f9e886316c",
                "environment": {
                    "id": "24f17b5b-a458-4238-973e-0f77401897ed"
                },
                "account": {
                    "canAuthenticate": true,
                    "status": "OK"
                },
                "address": {
                    "streetAddress": "115 Randy Park",
                    "locality": "Cookshire-Eaton",
                    "countryCode": "CA"
                },
                "createdAt": "2024-02-14T16:28:27.845Z",
                "email": "hearthfield_berengere@example.com",
                "enabled": true,
                "identityProvider": {
                    "type": "PING_ONE"
                },
                "lifecycle": {
                    "status": "ACCOUNT_OK"
                },
                "mfaEnabled": false,
                "name": {
                    "formatted": "Hearthfield Bérengère",
                    "given": "Hearthfield",
                    "family": "Bérengère"
                },
                "population": {
                    "id": "5a899957-18d8-4a62-9a15-e7d4a5265bf8"
                },
                "updatedAt": "2024-02-14T16:28:27.845Z",
                "username": "hearthfield_bérengère",
                "verifyStatus": "NOT_INITIATED"
            },
        ]
    }
}

The script converts this data to a structure similar to the example below and saves it to a users.json file in the same folder as the script.

{
	"users": [
		{
			"active": true,
			"birthDate": null,
			"insertInstance": null,
			"data": {
				"migrated": true,
				"favoriteColors": null
			},
			"email": "hearthfield_berengere@example.com",
			"expiry": null,
			"firstName": "",
			"fullName": "",
			"id": "916b9057-1de6-4718-931d-91f9e886316c",
			"lastLoginInstant": 0,
			"lastName": "",
			"middleName": "",
			"mobilePhone": "",
			"password": null,
			"salt": null,
			"factor": 10000,
			"encryptionScheme": "salted-pbkdf2-hmac-sha256",
			"passwordChangeRequired": false,
			"passwordLastUpdateInstant": 0,
			"preferredLanguages": [
				"en"
			],
			"identityProviders": {
			},
			"timezone": null,
			"twoFactorEnabled": false,
			"username": "hearthfield_bérengère",
			"verified": true
		},
		{
			"active": true,
			"birthDate": null,
			"insertInstance": null,
			"data": {
				"migrated": true,
				"favoriteColors": null
			},
			"email": "wingar_helena@example.com",
			"expiry": null,
			"firstName": "",
			"fullName": "",
			"id": "a7ff34b6-dfa1-4d60-abd7-5614eb21c8dc",
			"lastLoginInstant": 0,
			"lastName": "",
			"middleName": "",
			"mobilePhone": "",
			"password": null,
			"salt": null,
			"factor": 10000,
			"passwordChangeRequired": false,
			"passwordLastUpdateInstant": 0,
			"preferredLanguages": [
				"en"
			],
			"identityProviders": {
			},
			"timezone": null,
			"twoFactorEnabled": false,
			"username": "wingar_hélèna",
			"verified": true
		}
	]
}

Check the output for any errors and to make sure the data looks correct.

If you encounter any issues, you can modify the export program to output more information about the users being exported.

Importing Users

Remember that the steps outlined here for importing users will not export password hashes, and users will need to change their passwords manually in FusionAuth.

Here are the steps you need to take to import the user data.

  1. Set up FusionAuth.
  2. Get the script.
  3. Install needed gems.
  4. Use the script.
  5. Verify the import.

Set Up FusionAuth

You need to set up FusionAuth so migrated user data can be stored. As mentioned above, this guide assumes you have FusionAuth installed.

If you don’t, view our installation guides and install it before proceeding further.

Create A Test Tenant

It is best to create a separate tenant for migration testing. Tenants logically isolate configuration settings and users. If a migration goes awry or you need to redo it after tweaking settings, you can delete the test tenant and start with a clean system. To add a tenant, navigate to Tenants and choose the Add button (green plus sign).

Adding a tenant.

Give it a descriptive Name like Ping Identity import test. You shouldn’t need to modify any of the other configuration options to test importing users.

Save the tenant.

The tenant creation screen.

Record the Id of the tenant, which will be a UUID. It will look something like 25c9d123-8a79-4edd-9f76-8dd9c806b0f3. You’ll use this later.

The tenant list.

Create A Test Application

Applications are anything a user can log in to. In FusionAuth there’s no differentiation between web applications, SaaS applications, APIs and native apps. To add an application, navigate to Applications and click on the Add button (the green plus sign). Give the application a descriptive name like Ping Identity application.

Select your new tenant, created above, in the dropdown for the Tenant field.

Navigate to the OAuth tab and add an entry to Authorized redirect URLs . Use a dummy value such as https://example.com. Later, you’ll need to update this to be a valid redirect URL that can take the authorization code and exchange it for a token. Learn more about this in the FusionAuth OAuth documentation.

You shouldn’t need to modify any of the other configuration options to test importing users. Save the application.

The application creation screen.

Next, view the application by clicking the green magnifying glass and note the OAuth IdP login URL . You’ll be using it to test that users can log in.

Finding the login URL.

Add An API Key

The next step is to create an API key. This will be used by the import script. To do so, navigate to Settings -> API Keys in the administrative user interface.

Adding an API key

This key needs to have the permission to run a bulk import of users. In the spirit of the principle of least privilege, give it only the following permissions:

  • POST to the /api/user/import endpoint.
  • POST to the /api/user/search endpoint.
  • POST to the /api/identity-provider/link endpoint (not shown below).

Record the API key string, as you’ll use it below.

Setting API key permissions

Get The Script

FusionAuth provides an import script under a permissive open source license. It requires ruby (tested with ruby 2.7). To get the script, clone the git repository:

Getting the import scripts

git clone https://github.com/FusionAuth/fusionauth-import-scripts

Navigate to the pingidentity directory:

cd fusionauth-import-scripts/pingidentity

Install Needed Gems

The following gems must be available to the import script:

  • date
  • json
  • optparse
  • securerandom
  • fusionauth_client

All of these will likely be on your system already, except the fusionauth_client gem.

If you have bundler installed, run bundle install in the pingidentity directory. Otherwise, install the needed gems another way.

Use The Script

You can see the output of the script by running it with the -h option.

Running the import script with the help command line switch

ruby ./import.rb -h

The output will be similar to the following.

The help output of the import.rb script

Usage: import.rb [options]
    -l, --link-social-accounts       Link social accounts, if present, after import. This operation is slower than an import.
    -r APPLICATION_IDS,              A comma separated list of existing applications Ids. All users will be registered for these applications.
        --register-users
    -o, --only-link-social-accounts  Link social accounts with no import.
    -u, --users-file USERS_FILE      The exported JSON user data file from IdentityServer. Defaults to users.json.
    -f FUSIONAUTH_URL,               The location of the FusionAuth instance. Defaults to http://localhost:9011.
        --fusionauth-url
    -k, --fusionauth-api-key API_KEY The FusionAuth API key.
    -t TENANT_ID,                    The FusionAuth tenant id. Required if more than one tenant exists.
        --fusionauth-tenant-id
    -h, --help                       Prints this help.

For the script to work correctly, set the following switches, unless the defaults work for you.

  • -u should point to the location of the user export file you obtained, unless the default works.
  • -f should point to your FusionAuth instance. If you are testing locally, it will probably be http://localhost:9011.
  • -k should be set to the value of the API key created above.
  • -t should be set to the Id of the testing tenant created above.

The -o and -l switches will attempt to create links for any users authenticated via Google or another social identity provider found in the user data file.

If you are loading users with social account authentication, you must create the social identity providers in FusionAuth beforehand or the links will fail. Additionally, creating a link is not currently optimized in the same way that loading a user is. It may make sense to import all the users in one pass (omitting the -l switch) and then create the links using the -o switch in a second pass after the users are imported.

The social account linking functionality will only work with FusionAuth version 1.28 or above. The fusionauth_client library must be >= 1.28.

When you run the script, you should get an output similar to the following.

Import script output

$ ruby ./import.rb -f http://localhost:9011 -k '...' -t '...' -u users.json
FusionAuth Importer : IdentityServer
 > User file: users.json
  >> 2 users found in JSON file
 > Call FusionAuth to import users
 > Import success
Duplicate users 0
Import complete. 2 users imported.

Enhancing The Script

You may want to migrate additional data. Currently, the following attributes are migrated:

  • user_id
  • email
  • email_verified
  • username
  • insertInstance
  • registrations, if supplied

The migrated user will have the original Ping Identity user Id. If you have additional user attributes to migrate, review and modify the map_user method.

You may also want to assign roles or associate users with groups by creating the appropriate JSON data structures in the import call. These are documented in the Import User API docs. This will require modifying the import.rb code.

Verify The Bulk Import

Log in to the FusionAuth administrative user interface and review the user entries to ensure the data was correctly imported.

List imported users.

The Final Destination of Bulk Imported Users

After you are done testing, you can choose to import users into the default tenant or a new tenant. Whichever you choose, make sure to update the -t switch to the correct value before running the import for the final time.

If you aren’t keeping users in the test tenant, delete it.

If you need to start over because the import failed or you need to tweak a setting, delete the tenant you created. This will remove all the users and other configurations for this tenant, giving you a fresh start. To do so, navigate to Tenants and choose the Delete button (red trash can icon).

Deleting a tenant.

Confirm your desire to delete the tenant. Depending on how many users you have imported, this may take some time.

Now that you have all the users imported, you need to reset their passwords.

Reset Passwords

Users may reset their password using the Forgot Password link on the login page, or you can use the API calls documented below to do it.

This script will send an email to every imported user. Ensure your SMTP server can handle the volume. Configure your SMTP server in Tenants -> Your Tenant -> Email.

Review the Change Password template to ensure the messaging is correct. Here is more information on modifying the template.

Retrieve the loginId from the Ping Identity JSON export files and place it in a single file. The loginId will either be the username or the email address. Update the script below to use the API Key created in Set Up FusionAuth step.

The script below will call the forgot password API for every user. It sleeps periodically to avoid overloading the SMTP server. Make sure the API Key has the POST to the api/user/forgot-password endpoint permission enabled.

Example User Password Reset

#!/bin/bash
API_KEY=...
LOGIN_ID_FILE=...
FA_HOST=...

for loginId in `cat $LOGIN_ID_FILE`; do
  sleep $[( $RANDOM % 10 > 8)] # sleeps 1 second ~10% of the time
  RES=`curl --max-time 600 \
       -s -w "%{http_code}" \
       -H "Authorization: $API_KEY" \
       -H "Content-type: application/json" \
       -XPOST \
       $FA_HOST/api/user/forgot-password \
       -d '{"loginId": "'$loginId'","sendForgotPasswordEmail": true}'`
  if [ "$RES" -ne "200" ]; then
    echo "Error: $RES";
    exit 1;
  fi
done

At the end of this process, you have imported the user data and enabled users to reset their passwords to a known value.

Additional Support

If you need support in your migration beyond that provided in this guide, you may: