Monday, August 10, 2015

Setting Challenge Questions for Users in WSO2 Identity Server for Account Recovery

WSO2 Identity Server supports user account recover with challenge (security) questions. Here, users can set challenge questions for their accounts and with the challenge questions, they can recover their accounts [1] later in a situation like they forget the password for the account.

In this example, I use WSO2 Identity Server 5.0.0 with Service Pack 1 installed. Here I demonstrate following three ways to set challenge questions for a user account.
  1. Using Identity Server Dashboard
  2. Using UserIdentityManagementAdminService SOAP API
  3. Using RemoteUserStoreManagerService SOAP API

First, we need to set the required claims for this feature. Go to Configure -> Claim Management and go to WSO2 Claim Dialect (http://wso2.org/claims).


We need to set three claim mappings. These are already set by default. If not set them as shown below.

These are the three claims necessary. For each claim uri below, there should be one claim mapping.


Here for the mapped attribute, you can given any attribute that your underlying user store supports. Here I will keep the defaults as they are. I have set the three claims as ‘Supported by Default’ so that these attributes will appear in the user’s profile when viewing from the management console.

The usage of these claims are as below.

Claim URI
Description

This claim will hold the claim uris of the challenge question sets associated with the user’s profile. If the user has challenge question 1 and 2 set, the value will be all the associated claims separated by ! mark.
eg: http://wso2.org/claims/challengeQuestion1!http://wso2.org/claims/challengeQuestion2
This claim will hold the details of the first challenge question of the user. Here the claim value will contain the question and the answer. The answer is a hashed value and not in plain text.
eg : Favorite sport ?!jyf0Mvy6pLUYChzHqPoWapPNo8G85vGZIt1RnQL0uzk=
This claim will hold the details of the second challenge question of the user. Here the claim value will contain the question and the answer. The answer is a hashed value and not in plain text.
eg : Favorite food ?!IJ92QY7OfJNrZf9Hd6V42GD3YsN61sfwj1gmJCGZ71E=

Now the required claim mappings for challenge questions feature are set. Let’s see how we can set challenge questions for a user.

1. Set Challenge Questions for a User from Identity Server Dashboard

If the challenge questions are set from the Dashboard, the users themselves have to set their challenge questions for their accounts. First, I have created a user ‘tharindu’ from management console.


Here I demonstrate How a user can set challenge questions for his/her account using the Identity Server Dashboard [2]. The URL for accessing dashboard is following if the hostname is localhost and the Identity Server running port is 9443.  https://localhost:9443/dashboard/.


Login to the Identity Server from the user account you need to set challenge questions.



Go to the Account Recovery section and View Details.


Here you can set challenge questions for the user account. There are two sets of challenge questions by default. You can pick one question for each set and give an answer for the question.

After setting the challenge questions, let’s check the user’s profile from management console to verify that the challenge questions are set correctly. Here I login to the management console as ‘admin’ and view the user’s profile.






For the challenge question claims I can see the values are correctly set.


Here the admin cannot see the answers for the challenge questions as they are not appearing as plain text values.

Favorite food ?!IJ92QY7OfJNrZf9Hd6V42GD3YsN61sfwj1gmJCGZ71E=
Favorite sport ?!jyf0Mvy6pLUYChzHqPoWapPNo8G85vGZIt1RnQL0uzk=



2. Set Challenge Questions for a User from UserIdentityManagementAdminService

WSO2 Identity Server provides the UserIdentityManagementAdminService API for Identity Management related functions of user accounts. You can access the WSDL for the service from the following URL. (You need to set <HideAdminServiceWSDLs> false </HideAdminServiceWSDLs> property in <IS_HOME>/repository/conf/carbon.xml file to view the WSDLs of the services)


Here the admin user can set the challenge questions for a user unlike setting challenge questions with the Dashboard.

First, we can call the getAllChallengeQuestions method from a SOAP client like SOAP UI and get to know all the available challenge questions.

This is the sample SOAP request we need to send.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://services.mgt.identity.carbon.wso2.org">
  <soapenv:Header/>
  <soapenv:Body>
     <ser:getAllChallengeQuestions/>
  </soapenv:Body>
</soapenv:Envelope>

Here we get the following SOAP response.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
     <ns:getAllChallengeQuestionsResponse xmlns:ns="http://services.mgt.identity.carbon.wso2.org" xmlns:ax2308="http://mgt.identity.carbon.wso2.org/xsd" xmlns:ax2310="http://dto.mgt.identity.carbon.wso2.org/xsd">
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>City where you were born ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion1</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Father's middle name ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion1</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Favorite food ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion1</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Favorite vacation location ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion1</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Model of your first car ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion2</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Name of the hospital where you were born ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion2</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Name of your first pet ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion2</ax2310:questionSetId>
        </ns:return>
        <ns:return xsi:type="ax2310:ChallengeQuestionDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:order>0</ax2310:order>
           <ax2310:promoteQuestion>false</ax2310:promoteQuestion>
           <ax2310:question>Favorite sport ?</ax2310:question>
           <ax2310:questionSetId>http://wso2.org/claims/challengeQuestion2</ax2310:questionSetId>
        </ns:return>
     </ns:getAllChallengeQuestionsResponse>
  </soapenv:Body>
</soapenv:Envelope>

We need to look at questionSetId and question elements in the response. From each question set, we can pick one question to be set to the user’s profile as the challenge questions. Here I select the following two questions from set1 and set2.

Favorite food ?

Favorite sport ?


Now I call setChallengeQuestionsOfUser method to set these two challenge questions for the user. This is the SOAP request I need to send.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://services.mgt.identity.carbon.wso2.org" xmlns:xsd="http://dto.mgt.identity.carbon.wso2.org/xsd">
  <soapenv:Header/>
  <soapenv:Body>
     <ser:setChallengeQuestionsOfUser>
        <ser:userName>edirisinghe</ser:userName>
        <!--Zero or more repetitions:-->

        <ser:challengesDTOs>
           <xsd:answer>rice</xsd:answer>
           <xsd:id>http://wso2.org/claims/challengeQuestion1</xsd:id>
           <xsd:order>0</xsd:order>
           <xsd:primary>false</xsd:primary>
           <xsd:question>Favorite food ?</xsd:question>
           <xsd:verfied>false</xsd:verfied>
        </ser:challengesDTOs>

        <ser:challengesDTOs>
           <xsd:answer>soccer</xsd:answer>
           <xsd:id>http://wso2.org/claims/challengeQuestion2</xsd:id>
           <xsd:order>1</xsd:order>
           <xsd:primary>false</xsd:primary>
           <xsd:question>Favorite sport ?</xsd:question>
           <xsd:verfied>false</xsd:verfied>
        </ser:challengesDTOs>

     </ser:setChallengeQuestionsOfUser>
  </soapenv:Body>
</soapenv:Envelope>

After successfully setting the challenge questions for the user, we get the following SOAP response.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
     <ns:setChallengeQuestionsOfUserResponse xmlns:ns="http://services.mgt.identity.carbon.wso2.org">
        <ns:return xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
     </ns:setChallengeQuestionsOfUserResponse>
  </soapenv:Body>
</soapenv:Envelope>


Now we can verify that the challenge questions are correctly set by calling the getChallengeQuestionsOfUser method. This is the SOAP request we need to send.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://services.mgt.identity.carbon.wso2.org">
  <soapenv:Header/>
  <soapenv:Body>
     <ser:getChallengeQuestionsOfUser>
        <ser:userName>edirisinghe</ser:userName>
     </ser:getChallengeQuestionsOfUser>
  </soapenv:Body>
</soapenv:Envelope>


This is the SOAP response we get. For the answer, we see the hashed value instead of plain text answer.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
     <ns:getChallengeQuestionsOfUserResponse xmlns:ns="http://services.mgt.identity.carbon.wso2.org" xmlns:ax2308="http://mgt.identity.carbon.wso2.org/xsd" xmlns:ax2310="http://dto.mgt.identity.carbon.wso2.org/xsd">
        <ns:return xsi:type="ax2310:UserChallengesDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:answer>IJ92QY7OfJNrZf9Hd6V42GD3YsN61sfwj1gmJCGZ71E=</ax2310:answer>
           <ax2310:error xsi:nil="true"/>
           <ax2310:id>http://wso2.org/claims/challengeQuestion1</ax2310:id>
           <ax2310:key xsi:nil="true"/>
           <ax2310:order>0</ax2310:order>
           <ax2310:primary>false</ax2310:primary>
           <ax2310:question>Favorite food ?</ax2310:question>
           <ax2310:verfied>false</ax2310:verfied>
        </ns:return>
        <ns:return xsi:type="ax2310:UserChallengesDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2310:answer>jyf0Mvy6pLUYChzHqPoWapPNo8G85vGZIt1RnQL0uzk=</ax2310:answer>
           <ax2310:error xsi:nil="true"/>
           <ax2310:id>http://wso2.org/claims/challengeQuestion2</ax2310:id>
           <ax2310:key xsi:nil="true"/>
           <ax2310:order>1</ax2310:order>
           <ax2310:primary>false</ax2310:primary>
           <ax2310:question>Favorite sport ?</ax2310:question>
           <ax2310:verfied>false</ax2310:verfied>
        </ns:return>
     </ns:getChallengeQuestionsOfUserResponse>
  </soapenv:Body>
</soapenv:Envelope>

We can verify this from viewing the profile in the management console as well.


Set Challenge Questions with RemoteUserStoreManagerService

As the challenge questions and answers for users are stored as claims, we can think of setting the challenge questions of the users by updating the claims of the user using RemoteUserStoreManagerService.

If we call getUserClaimValues method, using the following SOAP request, we can see the challenge question details set for the given user.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://service.ws.um.carbon.wso2.org">
  <soapenv:Header/>
  <soapenv:Body>
     <ser:getUserClaimValues>
        <ser:userName>tharindu</ser:userName>
        <ser:profileName>default</ser:profileName>
     </ser:getUserClaimValues>
  </soapenv:Body>
</soapenv:Envelope>

This is the SOAP response we get (Other claims are removed from the response for readability).

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
     <ns:getUserClaimValuesResponse xmlns:ns="http://service.ws.um.carbon.wso2.org" xmlns:ax2603="http://api.user.carbon.wso2.org/xsd" xmlns:ax2602="http://core.user.carbon.wso2.org/xsd" xmlns:ax2610="http://tenant.core.user.carbon.wso2.org/xsd" xmlns:ax2608="http://dao.service.ws.um.carbon.wso2.org/xsd" xmlns:ax2606="http://common.mgt.user.carbon.wso2.org/xsd">

        <ns:return xsi:type="ax2608:ClaimDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2608:claimUri>http://wso2.org/claims/challengeQuestionUris</ax2608:claimUri>
           <ax2608:description xsi:nil="true"/>
           <ax2608:dialectURI xsi:nil="true"/>
           <ax2608:displayOrder>0</ax2608:displayOrder>
           <ax2608:displayTag xsi:nil="true"/>
           <ax2608:regEx xsi:nil="true"/>
           <ax2608:required>false</ax2608:required>
           <ax2608:supportedByDefault>false</ax2608:supportedByDefault>            <ax2608:value>http://wso2.org/claims/challengeQuestion1!http://wso2.org/claims/challengeQuestion2</ax2608:value>
        </ns:return>

        <ns:return xsi:type="ax2608:ClaimDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2608:claimUri>http://wso2.org/claims/challengeQuestion2</ax2608:claimUri>
           <ax2608:description xsi:nil="true"/>
           <ax2608:dialectURI xsi:nil="true"/>
           <ax2608:displayOrder>0</ax2608:displayOrder>
           <ax2608:displayTag xsi:nil="true"/>
           <ax2608:regEx xsi:nil="true"/>
           <ax2608:required>false</ax2608:required>
           <ax2608:supportedByDefault>false</ax2608:supportedByDefault>
           <ax2608:value>Favorite sport ?!jyf0Mvy6pLUYChzHqPoWapPNo8G85vGZIt1RnQL0uzk=</ax2608:value>
        </ns:return>
         <ns:return xsi:type="ax2608:ClaimDTO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
           <ax2608:claimUri>http://wso2.org/claims/challengeQuestion1</ax2608:claimUri>
           <ax2608:description xsi:nil="true"/>
           <ax2608:dialectURI xsi:nil="true"/>
           <ax2608:displayOrder>0</ax2608:displayOrder>
           <ax2608:displayTag xsi:nil="true"/>
           <ax2608:regEx xsi:nil="true"/>
           <ax2608:required>false</ax2608:required>
           <ax2608:supportedByDefault>false</ax2608:supportedByDefault>
           <ax2608:value>Favorite food ?!IJ92QY7OfJNrZf9Hd6V42GD3YsN61sfwj1gmJCGZ71E=</ax2608:value>
        </ns:return>
     </ns:getUserClaimValuesResponse>
  </soapenv:Body>
</soapenv:Envelope>

For setting the challenge questions, we can call setUserClaimValues method. This is the SOAP request I send.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://service.ws.um.carbon.wso2.org" xmlns:xsd="http://common.mgt.user.carbon.wso2.org/xsd">
  <soapenv:Header/>
  <soapenv:Body>
     <ser:setUserClaimValues>
        <ser:userName>john</ser:userName>
        <!--Zero or more repetitions:-->
        <ser:claims>
           <xsd:claimURI>http://wso2.org/claims/challengeQuestionUris</xsd:claimURI>
           <xsd:value>http://wso2.org/claims/challengeQuestion1!http://wso2.org/claims/challengeQuestion2</xsd:value>
        </ser:claims>
        <ser:claims>
           <xsd:claimURI>http://wso2.org/claims/challengeQuestion1</xsd:claimURI>
           <xsd:value>Favorite food ?!IJ92QY7OfJNrZf9Hd6V42GD3YsN61sfwj1gmJCGZ71E=</xsd:value>
        </ser:claims>
        <ser:claims>
           <xsd:claimURI>http://wso2.org/claims/challengeQuestion2</xsd:claimURI>
           <xsd:value>Favorite sport ?!jyf0Mvy6pLUYChzHqPoWapPNo8G85vGZIt1RnQL0uzk=</xsd:value>
        </ser:claims>       
        <ser:profileName>default</ser:profileName>
     </ser:setUserClaimValues>
  </soapenv:Body>
</soapenv:Envelope>

In the above request, the claim values has the following format.

Question text ?!EncodedHashOfAnswer

Here you can obtain the bytes of the string of the plain text answer, get the SHA-256 hash of that and encode to Base64. This way you can obtain the value to be included in the above (shown in blue color above). For the same, you can use following java code where value contains the plain text answer.

MessageDigest dgst = MessageDigest.getInstance("SHA-256");
byte[] byteValue = dgst.digest(value.getBytes());
return Base64.encode(byteValue);

Once the claims are updated, you can check them from the management console by viewing the user’s profile.

References :


Tharindu Edirisinghe
Identity Server Team
WSO2

No comments:

Post a Comment