PingFederate Server

Runtime behavior implementation

After you specify your plugin’s API at least partially, you can start implementing the runtime behavior. Use the specification that you defined previously to implement the runtime functionality.

Follow this pattern in lookupAuthN():

  1. Check for the possible actions the adapter expects in the current state.

  2. If an action is matched, then try to extract the expected model from the request and handle the action.

  3. If an action is requested, but it does not match an action allowed for the current state, then return an INVALID_ACTION_ID error.

  4. If no action is requested, render the response for the current state.

The AuthnApiSupport class provides much of the functionality for handling API requests and sending responses. The TemplateRenderAdapter stores a reference to this singleton in its apiSupport field.

private AuthnApiSupport apiSupport = AuthnApiSupport.getDefault();

Checking for actions

The following code shows the preferred approach for checking for the submitIdentifiers action.

The adapter performs this check two ways, depending on whether the current request is from the API endpoint. The TemplateRenderAdapter uses a slightly different but equivalent method.

/**
 * Determine if the user chose "Submit".
 */
private boolean isSubmitAttributesRequest(HttpServletRequest req)
{
	if (apiSupport.isApiRequest(req))
	{
		return ActionSpec.SUBMIT_USER_ATTRIBUTES.isRequested(req);
	}
	return StringUtils.isNotBlank(req.getParameter("pf.submit"));
}

Extracting models from requests

The next step extracts the model from the request. This step varies depending on whether the request is from the API endpoint. For an API request, call the AuthnApiSupport.deserializeAsModel() method. For a non-API request, you must build the model from the parameters in the request.

private SubmitUserAttributes getSubmittedAttributes(HttpServletRequest req) throws AuthnErrorException, AuthnAdapterException
{
	if (apiSupport.isApiRequest(req))
	{
		try
		{
			return apiSupport.deserializeAsModel(req, SubmitUserAttributes.class);
		}
		catch (IOException e)
		{
			throw new AuthnAdapterException(e);
		}
	}
	else
	{
		SubmitUserAttributes result = new SubmitUserAttributes();
		result.setUsername(req.getParameter("username"));

		for (String key : extendedAttr)
		{
			result.getUserAttributes().put(key, req.getParameter(key));
		}
		return result;
	}
}

The deserializeAsModel() method also does some validation on the incoming JSON. This includes checking for fields flagged as required in the model, using the @Schema annotation. If a validation error occurs during this step, the method throws an AuthnErrorException, which the adapter can convert to an API error response. For more information, see Handling authentication error exceptions.

Performing additional validation

The deserializeAsModel() method performs some basic validation on the submitted JSON. Your adapter probably needs to perform more validation and send an AuthnError to the API client if it finds any errors. Here is how the TemplateRenderAdapter validates the names of the provided user attributes:

private void validateSubmittedAttributes(HttpServletRequest req, SubmitUserAttributes submitted) throws AuthnErrorException
{
	if (apiSupport.isApiRequest(req))
	{
		List<AuthnErrorDetail> errorDetails = new ArrayList<>();
		for (String attrName : submitted.getUserAttributes().keySet())
		{
			if (!extendedAttr.contains(attrName))
			{
				errorDetails.add(ErrorDetailSpec.INVALID_ATTRIBUTE_NAME.makeInstanceBuilder()
					.message("Invalid attribute name: " + attrName).build());
			}
		}
		if (!errorDetails.isEmpty())
		{
			AuthnError authnError = CommonErrorSpec.VALIDATION_ERROR.makeInstance();
			authnError.setDetails(errorDetails);
			throw new AuthnErrorException(authnError);
		}
	}
}

Handling invalid action IDs

If a request from an API client includes an action ID that does not match any actions available in the current state, it is best practice to return an error to the client.

After checking all the possible actions, if none match and the request’s action ID is not null, the adapter can throw an AuthnErrorException. The adapter catches this exception and writes an error to the API response.

if (apiSupport.getActionId(req) != null)
{
	// An action ID was provided but it does not match one of those expected in the current state.
	throw new AuthnErrorException(CommonErrorSpec.INVALID_ACTION_ID.makeInstance());
}

Handling authentication error exceptions

If the deserializeAsModel() method detects an error while deserializing the model, it throws an AuthnErrorException. If the added validation checks in validateSubmittedAttributes detect an error, they also throw this exception.

The adapter should catch this exception and send an API error response using the method AuthnApiSupport.writeErrorResponse().

try
{
	...
}
catch (AuthnErrorException e)
{
	// A validation error occurred while processing an API request, return an error response to the API client
	apiSupport.writeErrorResponse(req, resp, e.getValidationError());
	authnAdapterResponse.setAuthnStatus(AUTHN_STATUS.IN_PROGRESS);
	return authnAdapterResponse;
}

Sending API responses

AuthnApiSupport provides several methods for writing API responses.

The following example shows how the TemplateRenderAdapter writes the response for the USER_ATTRIBUTES_REQUIRED state.

private void renderApiResponse(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> inParameters) throws AuthnAdapterException
{
	UserAttributesRequired model = new UserAttributesRequired();
	model.setAttributeNames(new ArrayList<>(extendedAttr));
	AuthnState<UserAttributesRequired> authnState = apiSupport.makeAuthnState(req, StateSpec.USER_ATTRIBUTES_REQUIRED, model);
	try
	{
		apiSupport.writeAuthnStateResponse(req, resp, authnState);
	}
	catch (IOException e)
	{
		throw new AuthnAdapterException(e);
	}
}

The makeAuthnState() method takes an AuthnStateSpec and an instance of the model for that state and builds an AuthnState object. The AuthnState object can then be further modified. For example, you could remove an action that is not currently applicable using the removeAction() method. Then you write the AuthnState object to the response using the writeAuthnStateResponse() method.

Returning authentication statuses

As with non-API requests, when the adapter finishes, it returns AUTHN_STATUS.SUCCESS or AUTHN_STATUS.FAILURE from lookupAuthN().

If the adapter has not yet finished and has written something to the response, it should return AUTHN_STATUS.IN_PROGRESS.