/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2016-2019 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.unboundid.directory.sdk.broker.api.IdentityAuthenticator; import com.unboundid.directory.sdk.broker.config.IdentityAuthenticatorConfig; import com.unboundid.directory.sdk.broker.types.AuthenticationRequest; import com.unboundid.directory.sdk.broker.types.AuthenticationResult; import com.unboundid.directory.sdk.broker.types.BrokerContext; import com.unboundid.directory.sdk.broker.types.ScimResourcePrincipal; import com.unboundid.directory.sdk.broker.types.StatusRequest; import com.unboundid.directory.sdk.broker.types.StatusResult; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.scim2.common.GenericScimResource; import com.unboundid.scim2.common.exceptions.ScimException; import com.unboundid.scim2.common.utils.JsonUtils; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.StringArgument; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * This example provides an example implementation of an Identity Authenticator * which provides additional assurance of an already authenticated user. * In the first step, the authenticator returns the set of security questions * in the user resource. In the second step, the authenticator checks that the * correct answer has been provided for the question the user selected to * answer. */ public class ExampleIdentityAuthenticator extends IdentityAuthenticator { // The name of the argument that will be used to specify the path to a // user resource attribute containing the set of security questions. private static final String ARG_NAME_QUESTION_ATTRIBUTE_PATH = "question-attribute-path"; // Handle to the ServerContext object. private BrokerContext serverContext; // The path of the user resource attribute containing the set of security // questions. private String attrPath; /** * {@inheritDoc} */ @Override public String getExtensionName() { return "Example Identity Authenticator"; } /** * {@inheritDoc} */ @Override public String[] getExtensionDescription() { return new String[] { "This example provides an example implementation of an Identity " + "Authenticator which provides additional assurance of an already " + "authenticated user. In the first step, the authenticator " + "returns the set of security questions in the user resource. In " + "the second step, the authenticator checks that the correct " + "answer has been provided for the question the user selected " + "to answer." }; } /** * {@inheritDoc} */ @Override public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { final String description = "The path to a multi-valued complex user resource attribute " + "containing the set of security questions. An example value " + "is { \"question\": \"What is your favorite color?\", " + "\"answer\": \"Yellow\" }"; final StringArgument attrPathArg = new StringArgument(null, ARG_NAME_QUESTION_ATTRIBUTE_PATH, true, 1, "{attr-path}", description); parser.addArgument(attrPathArg); } /** * Initializes this authenticator. Any initialization should be performed * here. This method should generally store the {@link BrokerContext} in * a class member so that it can be used elsewhere in the implementation. * * @param serverContext A handle to the server context for the server in * which this extension is running. Extensions should * typically store this in a class member. * @param config The general configuration for this object. * @param parser The argument parser which has been initialized from * the configuration for this authenticator. * @throws LDAPException If a problem occurs while initializing this * authenticator. */ @Override public void initializeAuthenticator(final BrokerContext serverContext, final IdentityAuthenticatorConfig config, final ArgumentParser parser) throws LDAPException { this.serverContext = serverContext; final StringArgument attrPathArg = (StringArgument) parser.getNamedArgument( ARG_NAME_QUESTION_ATTRIBUTE_PATH); attrPath = attrPathArg.getValue(); } /** * This hook is called when the Identity Authenticator is disabled or the * server shuts down. Any clean-up of this Identity Authenticator should * be performed here. * <p> * The default implementation is empty. */ @Override public void finalizeAuthenticator() { // No implementation is needed in this example. } /** * {@inheritDoc} */ @Override public StatusResult getStatus(final StatusRequest request) { if (request.getPrincipal() == null) { return StatusResult.notReady().build(); } final Map<String, String> questionsAndAnswers; try { questionsAndAnswers = getQuestionsAndAnswers(request.getPrincipal()); } catch (ScimException e) { return StatusResult.notReady("serverError", e.getMessage()).build(); } if (questionsAndAnswers.isEmpty()) { return StatusResult.notReady().build(); } final ObjectNode responseParams = JsonUtils.getJsonNodeFactory().objectNode(); // Return the current state for this authentication flow. ArrayNode questions = responseParams.putArray("questions"); for(String question : questionsAndAnswers.keySet()) { questions.add(question); } return StatusResult.ready() .setResponseParams(responseParams.toString()) .build(); } /** * {@inheritDoc} */ @Override public AuthenticationResult authenticate( final AuthenticationRequest request) { // This is the second step where we check the answer provided. if (request.getPrincipal() == null) { return AuthenticationResult.failure("serverError", "The example authenticator requires an " + "authenticated principal").build(); } final ObjectNode requestParams = toObjectNode(request.getRequestParameters()); final String selectedQuestion = requestParams.path("question").textValue(); final String providedAnswer = requestParams.path("answer").textValue(); if(selectedQuestion == null || providedAnswer == null) { return AuthenticationResult.failure("badRequest", "A selected question and the answer must be specified").build(); } final Map<String, String> questionsAndAnswers; try { questionsAndAnswers = getQuestionsAndAnswers(request.getPrincipal()); } catch (ScimException e) { return AuthenticationResult.failure("serverError", e.getMessage()).build(); } // Find the answer to the selected question. String expectedAnswer = null; for(Map.Entry<String, String> questionAndAnswer : questionsAndAnswers.entrySet()) { if(questionAndAnswer.getKey().equals(selectedQuestion)) { expectedAnswer = questionAndAnswer.getValue(); break; } } final ObjectNode responseParams = JsonUtils.getJsonNodeFactory().objectNode(); ArrayNode questions = responseParams.putArray("questions"); for(String question : questionsAndAnswers.keySet()) { questions.add(question); } if(expectedAnswer == null) { return AuthenticationResult.failure("badRequest", "The selected question is invalid"). setResponseParams(responseParams.toString()).build(); } if (expectedAnswer.equalsIgnoreCase(providedAnswer.trim())) { // Correct answer. return AuthenticationResult.success(request.getPrincipal()) .setResponseParams(responseParams.toString()) .build(); } else { // Incorrect answer. return AuthenticationResult.failure("incorrectAnswer", null) .setResponseParams(responseParams.toString()) .build(); } } /** * Retrieve the question and answers from the provided user. * * @param principal The user whose questions and answers to retrieve. * @return The questions and answers. * @throws ScimException If an error occurs during retrieval. */ private Map<String, String> getQuestionsAndAnswers( final ScimResourcePrincipal principal) throws ScimException { final JsonNode node; // This is the first step where we choose a question from those available // in the principal's SCIM resource. final GenericScimResource scimResource = serverContext.getInternalScimInterface().retrieve( principal.getResourceEndpoint(), principal.getResourceId(), GenericScimResource.class); node = scimResource.getValue(attrPath); final Map<String, String> questionsAndAnswers = new HashMap<String, String>(); if (node.isArray()) { final ArrayNode arrayNode = (ArrayNode) node; for (JsonNode n : arrayNode) { if (n.isObject()) { ObjectNode objectNode = (ObjectNode)n; final String question = objectNode.path("question").asText(); final String answer = objectNode.path("answer").asText(); if (!question.isEmpty() && !answer.isEmpty()) { questionsAndAnswers.put(question, answer); } } } } return questionsAndAnswers; } /** * Parse a JSON object in string form into an ObjectNode. * * @param jsonString The string containing a JSON object. * @return The parsed ObjectNode. */ private ObjectNode toObjectNode(final String jsonString) { if (jsonString == null) { return null; } try { return (ObjectNode)JsonUtils.getObjectReader().readTree(jsonString); } catch (IOException e) { return null; } } }