.. yubienroll-pingone-aic.rst .. _pingone-aic-config: ============================================== Configuring YubiEnroll with PingID PingOne AIC ============================================== The following describes how to set up YubiEnroll in the PingOne Advanced Identity Cloud Application (AIC) tenants and configure the required user permissions. Configuration Steps =================== The configuration steps involve the following: 1. :ref:`pingone-aic-register-oauth` 2. :ref:`pingone-aic-journey` 3. :ref:`pingone-aic-permissions` 4. :ref:`pingone-aic-register-in-yubienroll` When you have successfully completed these steps, you are ready to :ref:`enroll YubiKeys on behalf of end users in your organization `. .. _pingone-aic-register-oauth: Registering the YubiEnroll App ================================ To use PingOne AIC in YubiEnroll you must first register an OAuth2 Client in your realm. Complete the steps: #. Create a new application. a. Login to the PingOne AIC Admin Console. b. From the left panel, select **Applications**. c. Select **Custom applications** to add a new application. d. Select Sign-in Method, **OIDC - OpenID Connect**. e. Select Application Type, **Native/SPA**. f. Enter a Tenant Name and Owner for the app. g. Click **Next**. #. Set the Applications > Sign-on tab configuration. a. For Sign-In URLs/Redirect URI. Specifying port seems to work best. For example, http://localhost:8443/yubienroll-redirect b. Select Grant Types, **Authorization Code** and **Refresh Token**. c. Select Scopes, **openid**, **profile**, and **fr:idm:\***. d. Select **Show advanced settings**. #. Set the Applications > Authentication tab configuration. a. Set Token Endpoint Authentication Method to **None**. b. Enable **Use a journey to authenticate users to this application.**. Select an appropriate authentication journey. For example **Login**. .. image:: graphics/ping-aic-applications.png :width: 600 #. Click **Save** to push the updates. .. _pingone-aic-journey: Creating a WebAuthn Registration Journey ========================================== To enable WebAuthn registration with PingOne AIC in YubiEnroll, you must first create a WebAuthn registration journey. While these journeys are highly customizable, YubiEnroll requires a specific configuration to ensure seamless API communication with the IDP. Journeys in PingOne AIC are highly customizable. Since YubiEnroll does not provide such flexibility on API communication with the Identify Provider (IdP) we need to enforce some requirements. Journey Structure ------------------ The journey must follow this structure: .. code-block:: Start → Username Collection → [Server-side nodes] → WebAuthn Registration Node → Success ↓ (false) Failure For example: .. code-block:: Start → Page Node (Platform Username) → Identify Existing User → Scripted Decision Node → WebAuthn Registration Node → Success ↓ (false) Failure The following image lists possible errors along the path to a successful journey. .. image:: graphics/pinone-aic-journey.png :width: 800 Node 1 — Username collection ----------------------------- The first node in the journey must collect the username. You can use either a **Page Node** containing a **Platform Username** node, or a standalone **Username Collector** node. YubiEnroll submits the ``userName`` of the target user into this node. You can then add server-side nodes such as **Identify Existing User**, additional **Scripted Decision Nodes**, or **Inner Tree Evaluator** nodes to process the user identity before reaching the WebAuthn Registration Node. Node 2 — Scripted Decision Node (authorization) ------------------------------------------------- YubiEnroll sends the operator's OAuth2 access token in the ``Authorization`` header on every call to the journey. We recommend adding a **Scripted Decision Node** to verify that the operator is authorized to register credentials on behalf of other users. Configure the node with two outcomes: ``Success`` (continues to the WebAuthn Registration Node) and ``Failure`` (routes to a Failure Node). The script needs to introspect the access token to identify the caller. This requires a separate OAuth2 client configured in PingOne AIC with the ``am-introspect-all-tokens`` scope. Store the client ID and client secret as ESV (Environment Secret & Variable) secrets so they are not hardcoded in the script. The following ESVs are used in the example below: .. table:: :class: longtable +---------------------------------------------+----------------------------------------------+ | ESV | Description | +=============================================+==============================================+ | ``esv.yubienroll.introspect.client.id`` || Client ID of the OAuth2 client used for | | || token introspection | +---------------------------------------------+----------------------------------------------+ | ``esv.yubienroll.introspect.client.secret`` || Client secret of the OAuth2 client used for | | || token introspection | +---------------------------------------------+----------------------------------------------+ | ``esv.yubienroll.tenant`` || The tenant hostname. For example, | | || ``mycompany.forgeblocks.com`` | +---------------------------------------------+----------------------------------------------+ Below is an example script that introspects the access token and checks whether the caller is a member of an authorized group. Adjust the group name and ESV keys to match your environment: .. code-block:: var NodeOutcome = { SUCCESS: "Success", FAILURE: "Failure" }; // ESV references — adjust these to match your environment var INTROSPECT_CLIENT_ID = "esv.yubienroll.introspect.client.id"; var INTROSPECT_CLIENT_SECRET = "esv.yubienroll.introspect.client.secret"; var TENANT = "esv.yubienroll.tenant"; // The group that authorized operators must be a member of var AUTHORIZED_GROUP_CN = "cn=webauthn-admins,"; (function () { try { // 1. Extract the Bearer token from the Authorization header var authHeader = requestHeaders.get("Authorization"); if (!authHeader || authHeader.isEmpty()) { logger.error("No Authorization header present"); action.goTo(NodeOutcome.FAILURE); return; } var token = authHeader.get(0).replace("Bearer ", ""); // 2. Introspect the token using the configured OAuth2 client var clientId = systemEnv.getProperty(INTROSPECT_CLIENT_ID); var clientSecret = systemEnv.getProperty(INTROSPECT_CLIENT_SECRET); var tenant = systemEnv.getProperty(TENANT); var introspectUrl = "https://" + tenant + "/am/oauth2/realms/root/realms/alpha/introspect"; var response = httpClient.send(introspectUrl, { method: "POST", form: { token: token, client_id: clientId, client_secret: clientSecret } }).get(); var result = response.json(); // 3. Verify the token is active if (String(result.active) !== "true") { logger.error("Access token is not active"); action.goTo(NodeOutcome.FAILURE); return; } // 4. Resolve the caller's identity var callerUsername = result.username; if (!callerUsername) { logger.error("No username in introspection response"); action.goTo(NodeOutcome.FAILURE); return; } var identity = idRepository.getIdentity(callerUsername); if (!identity.exists()) { logger.error("Identity not found for " + callerUsername); action.goTo(NodeOutcome.FAILURE); return; } // 5. Check group membership var memberOf = identity.getAttributeValues("isMemberOf"); var authorized = false; for (var i = 0; i < memberOf.length; i++) { if (String(memberOf[i]).startsWith(AUTHORIZED_GROUP_CN)) { authorized = true; break; } } if (authorized) { logger.info("Authorized: " + callerUsername); action.goTo(NodeOutcome.SUCCESS); } else { logger.error("Not authorized: " + callerUsername); action.goTo(NodeOutcome.FAILURE); } } catch (e) { logger.error("Authorization check error: " + e); action.goTo(NodeOutcome.FAILURE); } }()); Node 3 — WebAuthn Registration Node ------------------------------------- Add a **WebAuthn Registration Node** after the server-side nodes. Configure it with the appropriate relying party ID and attestation settings for your environment. YubiEnroll receives the registration challenge from this node, create the credential on the YubiKey, and submit the attestation result back. Terminal nodes ---------------- Connect the WebAuthn Registration Node to a **Success Node**. Connect the ``false`` outcome of the Scripted Decision Node to a **Failure Node**. Important notes ---------------- * Do not add nodes that produce callbacks (interactive nodes) between the username collection and the WebAuthn Registration Node. Server-side nodes such as additional Scripted Decision Nodes or Inner Tree Evaluator nodes are fine, as they execute without prompting the client. YubiEnroll expects exactly one username prompt followed by the WebAuthn registration challenge — any additional prompts cause the registration to fail. * The journey must be in the realm configured in YubiEnroll (the ``realm`` parameter in the provider configuration). .. _pingone-aic-permissions: Configuring Permissions ========================== To enroll YubiKeys on behalf of an end user, the YubiEnroll app user, for example an IT admin, must have the PingOne role: ``openidm-admin`` and have the correct permissions as defined by the WebAuthn Registration Journey (e.g. be a member of an authorized group). .. _pingone-aic-register-in-yubienroll: Preparing to Add the PingOne AIC Provider ========================================== When adding a provider configuration in YubiEnroll you need the following values from PingOne AIC. These were created in steps :ref:`pingone-aic-register-oauth` and :ref:`pingone-aic-journey`. The table lists where the values can be found. .. table:: :class: longtable +--------------------+------------------------------------------------------+ | Item | Location | +====================+======================================================+ | ``Client ID`` | The OAuth2 client you created | +--------------------+------------------------------------------------------+ | ``Redirect URI`` | The OAuth2 client you created | +--------------------+------------------------------------------------------+ | ``tenant`` || This is PingOne AIC tenant name. | | || Find it on the PingOne AIC Tenant Settings. | +--------------------+------------------------------------------------------+ | ``realm`` || This is the PingOne AIC realm where the client | | || was registered. | +--------------------+------------------------------------------------------+ | ``journey`` || This is the PingOne AIC WebAuthn registration name. | | || Copy the name from journey you created. | +--------------------+------------------------------------------------------+ To add PingOne AIC in YubiEnroll, see :ref:`idp-config-add`.