UFP Identity Integration


Introduction

The fundamental difference between standard web-site login and using the UFP Identity service to log your users in is that the login is split into two or more parts.

The first part collects the user id; typically a username or email. The second part collects authentication credentials that will be used to log the user in to your site.

Most sites have some HTML form that allows users to login and associates a session with a user account. The HTML code might look like:

<form action="auth/authentication.asp">
  <table>
    <tr>
      <th>Username</th>
      <td>
        <input type="text" name="username" size="20"/>
      </td>
    </tr>
    <tr>
      <th>Password</th>
      <td>
        <input type="password" name="password" size="20"/>
      </td>
    </tr>
    <tr><td colspan="2"><input type="submit" value="Login"/></td></tr>
  </table>
</form>
      

Which would render:

Username
Password

Subsequently, the typical login procedure might go as follows:

  • The username value from the form is used to look up account details associated with that username.
  • The account details have a password value which may be plain text or hashed.
  • If the password value from the account details is hashed, the password value from the form must also be hashed.
  • The password value from the form (either hashed or plain text) is compared with the password from the account details.
  • If the passwords match, the user is logged in. If the passwords don't match the user has failed to login.

In contrast, UFP Identity replaces the mechanism of comparing the user's password with the password value from the form.

The login process is split into two or more parts. This change in user behavior is so that users can use their preferred token(s), your site can leverage stronger authentication if necessary, and risk can be detected dynamically and mitigated.

With UFP Identity the login procedure goes as follows:

  • The username value from the form is sent to the UFP Identity service for pre-authentication.
  • The pre-authentication returns some UI elements to hint at what the user needs to enter to authenticate.
  • The UI displays the additional elements needed to proceed with authentication.
  • The values of the additional elements are sent to the UFP Identity service.
  • If success is returned, the user is logged in. If failure is returned, the user has failed to login. Other return values may elicit additional states.

First, collect the username...

Username

Then, using UI hints from the first call to UFP Identity, collect the authentication credentials for the user.

Username foo@bar.com
Passphrase

Getting Started

In order to use the UFP Identity service, you must connect with client-authenticated SSL. In order to use client-authenticated SSL you must generate a private/public key pair and a Certificate Signing Request to be signed by UFP Identity. You must also take care to protect the private key.

The following steps are required to get started. OpenSSL is used to perform the operations needed. (n.b. In the examples we use a fictional design company; magrathea.com. For your site you would use your site information)

  1. Create a strong key to protect the private key.

    There are a number of ways to create a key. The recommended way is to create a 16 byte random key containing only US-ASCII characters. The following script may be used to create a good, strong key.

    #!/bin/sh
    export LC_ALL=C
    head -c 200 /dev/random | tr -cd '[:graph:]' | head -c 16
                

    Using the above script saved to random.sh, you can create your secret key and save it to a file.

    random.sh > secret.key
              

    If you are absolutely sure the underlying cryptography is supplied by OpenSSL, you can create a key and save it to a file using OpenSSL.

    openssl rand 16 > secret.key
              
  2. Generate a private/public key pair...

  3. ... encrypted with the strong key from step 1.

    openssl genrsa -out magrathea.key.pem -passout file:secret.key -aes256 2048
              
  4. Generate a Certificate Signing Request (CSR).

    openssl req -new -passin file:secret.key -key magrathea.key.pem -out magrathea.csr.pem
            

    You will be asked a series of questions to populate the CSR. Please enter the information carefully.

    Notes on choosing a common name.

    The Common Name must be specified and unique. The common name is used to group logins for your site and is generally either a site name, a domain name or an email address. For example, for a development site on your own machine, an email (info@company.com) would be a desirable common name so you could apply all your logins to all your development sites. For distinct sites a fully qualified site name e.g. www.company.com would be appropriate for grouping all the logins for that site. If you wanted multiple sites to share logins, you could use the domain as a common name e.g. company.com. Please contact us if you have questions and we can help you decide the best fit for your needs.

    Notes on choosing an email contact.

    The email chosen should be the email of someone that can be contacted for the lifetime of the company. In general, we expect you to provide enough information so that we can contact you in the event of an issue. As a last resort, please choose an email that would provide the most likelihood of contact.

  5. Send the CSR (magrathea.csr.pem) off to info@ufp.com.

  6. Take care to keep the received Certificate (magrathea.crt.pem) safe.

    Make sure permissions on all .pem files are least privilege required and make sure that .pem files can never be retrieved via requests. Also make sure to properly and securely back up your private/public key pair (magrathea.key.pem), secret key (secret.key), CSR (magrathea.csr.pem), and certificate (magrathea.crt.pem).

  7. Take care to keep the secret key safe

    If you plan to store the secret key (secret.key) in a database we recommend Base64 encoding the key to store in the database. Remember to Base64 decode the key before passing it through for decryption of the private key.

    Base64 Encode:

    cat secret.key | openssl enc -e -a
              

    Base64 Decode:

    echo '1wvRVqVlGxovZT+dJoqaSw==' | openssl enc -d -a
              

    or, being careful of newlines that are NOT part of your key

    echo -n '1wvRVqVlGxovZT+dJoqaSw==' | openssl enc -d -a -A
              

Authentication

(n.b. For these examples we will be using curl to show how the service works. Because our secret key is a random string of bytes that can't be typed out, we are using the -K facility of curl to pass the secret key in)

Notes on using the UFP Identity service.

Every call to the service requires a query parameter named client_ip which is usually pulled off the client request. The CGI variable REMOTE_ADDR can be accessed from most web frameworks. The service will fail with HTTP code 400 Bad Request if the client_ip query parameter is not present.

For any authentication, the first call to the service looks like:

echo -n "pass=`cat secret.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathea.key.pem \
'https://identity.ufp.com/identity-services/services/preauthenticate?name=guest&client_ip=192.0.2.42'
      

returns the following authentication_pretext assuming the user 'guest' is found.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<authentication_pretext>
  <name>guest60e4ec24a11e4edf</name>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="authenticationResult" confidence="0.0" level="0" code="0" message="OK">SUCCESS</result>
  <display_item name="passphrase">
    <display_name>Password</display_name>
    <form_element>&lt;input id="AuthParam0" type="password" name="passphrase" /&gt;</form_element>
    <nickname>Guest Password</nickname>
  </display_item>
</authentication_pretext>
      

The name element represents the user id that the service found for the user id passed in. The user id returned in the name element MUST be used in subsequent calls to the service. Do NOT assume the user id passed in the from the user is the same as the user id found by the service.

Any number of display_item elements may be present, your UI should be able to deal with more than one.

The value of the form_element element has URL encoded form of the input parameter. This is so that the value of the form_element element can be placed directly in a form.

The name attribute of the display_item element indicates the name of the parameter, from the input element, that must be passed back.

There are no specific requirements for how the subsequent parameters are displayed but one suggested option is to take the value of the display_name element, the form_element element, and the nickname element and insert those into a subsequent form. e.g.

<form action="">
  <table>
    <tr>
      <th>Username</th>
      <td>guest60e4ec24a11e4edf</td>
    </tr>
    <tr>
      <th><abbr title="Guest Password">Passphrase</abbr></th>
      <td>&lt;input id="AuthParam0" type="password" name="passphrase" /&gt;</td>
    </tr>
    <tr><td colspan="2"><input type="submit" value="Login"/></td></tr>
  </table>
</form>
      

In the case the user is not found, an authentication_pretext indicating this will be returned:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<authentication_pretext>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="authenticationResult" confidence="0.0" level="0" code="4" message="User not found">FAILURE</result>
</authentication_pretext>
      

In this case, the UI could begin an enrollment flow to register the user as new. More details can be found in the Enrollment section.

Notes regarding state transitions between calls to the service.

The state of the authentication must be maintained by the application. Since the authentication is in two or more parts, the easiest mechanism is to utilize the client session to maintain some state variables. A good way to do this is to create two session variables; IDENTITY_USERNAME and IDENTITY_DISPLAY_ITEMS. Set the IDENTITY_USERNAME to the name element value from the authentication_pretext. Also unmarshall the display_item elements and serialize them to the IDENTITY_DISPLAY_ITEMS session variable.

When processing, on the front end, you can rely on the session variables either being present or not. If not, show the form asking for the username. If the session variables are present, show the form with the read-only user name, and iterate over the unmarshalled display items to show the necessary inputs. On the back end, if the session variables are not present you take the form element username and make the preauthenticate call to the service. If the session variables are present, you collect the names of the inputs you are expecting, pass the values and the name again to authenticate.

After the user submits the second form, we perform the second call to the service:

echo -n "pass=`cat secret.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem
--key magrathea.key.pem \
'https://identity.ufp.com/identity-services/services/authenticate?name=guest60e4ec24a11e4edf&client_ip=192.0.2.42&passphrase=guest'
      

returns an authentication_context in the case of a successful authentication:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<authentication_context>
  <name>guest1884fa4717f64f4e</name>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="authenticationResult" confidence="0.34" level="0" code="0" message="OK">SUCCESS</result>
</authentication_context>
      

Notes regarding successful authentication.

Upon receiving an authentication_context with a result value of "SUCCESS" and code attribute of 0, the authentication has succeeded and the user can be logged in. IMPORTANT: You *MUST* use the authentication_context's name element as the user logged in. NEVER use the user supplied name.

If the session variables IDENTITY_USERNAME and IDENTITY_DISPLAY_ITEMS were used, these should be removed at this time, and some session indication of the user being logged in should be saved.

The other attributes are used to further "qualify" the authentication in the case of a very high-value transaction or high-risk situation. The confidence attribute gives a relative confidence in the "quality" of the authentication. Details about this attribute are forthcoming. The level attribute is an indication of the relative level of the authentication tokens used. Generally a password that has been used for some time would have a low level and a true two-factor hardware authenticator would have a higher level. The code attribute is a numeric code representing the result of the authentication or preauthentication.

and in the case of a failed authentication:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<authentication_context>
  <name>guestd0e3636d44604d97</name>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="authenticationResult" confidence="0.0" level="0" code="9" message="Authentication failed">FAILURE</result>
</authentication_context>
      

Notes about other states that may be returned.

Two other states besides SUCCESS and FAILURE may be returned. In the case that the user's login context has been removed, a RESET state is returned. In the RESET case, the session variables IDENTITY_USERNAME and IDENTITY_DISPLAY_ITEMS should be cleared, any error messages should be cleared and the UI should be reset to ask for a user id only. In the case further authentication is required, a CONTINUE state is returned along with a new authentication_pretext with new display_item elements to be presented to the user. In the CONTINUE case, the session variable IDENTITY_DISPLAY_ITEMS should be reset with the new display_item elements.

Enrollment

Enrollment can be categorized into two main types; 1) where the user DOES NOT already exist and 2) where the user DOES already exist. All of the standard provisioning operations fall into one of these categories. The service offers the enroll operation for when the user DOES NOT already exist, and the reenroll operation for when the user DOES already exist. Further categorization of the call can be indicated with a query parameter named type. There is also a helper operation, preenroll, which allows you to determine if an enroll operation will potentially succeed, i.e. the user DOES NOT already exist.

Enrollments are usually customized for your site. The details of the parameters and returns can vary depending on the needs of your site.

Notes on enrollment customizations.

The customizations for enrollment are typically highly dependent on how your site(s) currently works. Because you potentially have existing users and have a specific way of handling their passwords, we can customize the enrollment to handle the way you currently do things.

For the following examples we are going to assume your site has collected username, email and password from the user and stored these values in a database row for each user. In addition we will assume you have stored the MD5 hash of the password with no salts.

For the customizations we would do, we would perform a mapping of query parameters as follows:

  • username => name
  • email => email
  • password => password_hash (in the case of existing users)
  • password => passphrase (in the case of new users)
Pre-enrollment

Preenrollment allows you to determine if a subsequent enrollment will succeed (usually checking if the user does not exist) and MAY return UI elements that would be presented to the user to collect enrollment information.

echo -n "pass=`cat secret.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathea.key.pem \
'https://identity.ufp.com/identity-services/services/preenroll?name=guest&client_ip=192.0.2.42'
      

returns an enrollment_pretext:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<enrollment_pretext>
  <name>guest</name>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="enrollmentResult" level="0" code="0" message="OK">SUCCESS</result>
  <form_element name="passphrase">
    <element>&lt;input id="EnrollParam0" type="text" name="passphrase" class="field required" /&gt;</element>
  </form_element>
</enrollment_pretext>
      
Enrollment

For enrollment, typically there are two subtypes that are handled; import and new. The subtypes are indicated by a type query parameter. Import is a synchronous operation allowing you to enroll an existing user in your system but not yet enrolled with UFP Identity. New is a synchronous operation that enrolls a new user.

echo -n "pass=`cat security.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathrea.key.pem \
'https://identity.ufp.com/identity-services/services/enroll?name=slartibartfast\
&client_ip=192.0.2.42&type=new&passphrase=test123&email=slartibartfast@magrathea.com'
      

or

echo -n "pass=`cat security.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathrea.key.pem \
'https://identity.ufp.com/identity-services/services/enroll?name=slartibartfast\
&client_ip=192.0.2.42&type=import&password_hash=cc03e747a6afbbcbf8be7668acfebee5&email=slartibartfast@magrathea.com'
      

returns an enrollment_context

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<enrollment_context>
  <name>slartibartfast</name>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="enrollmentResult" level="0" code="0" message="OK">SUCCESS</result>
</enrollment_context>
      
Re-enrollment

For re-enrollment there are also typically two subtypes that are handled; update and delete. Update is an update to an existing user (e.g. password change, email change, username change). Delete is a removal or de-provisioning of the user from your site.

An update may update any of the enrollment parameters. Any parameter that could potentially be considered an identifier (e.g. username, email) must have "new-" prepended to the query parameter being updated. For example:

echo -n "pass=`cat security.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathrea.key.pem \
'https://identity.ufp.com/identity-services/services/reenroll?name=slartibartfast\
&client_ip=192.0.2.42&type=update&passphrase=test1234&email=slartibartfast@magrathea.com&new-email=slarti@magrathea.com'
      

Would update both the email and the password for the user. If the name of the user were to be updated, a new-name would be required in addition to the name query parameter. In all cases, calling reenroll services assumes the user has already logged in or an administrator is performing the operations.

echo -n "pass=`cat security.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathrea.key.pem \
'https://identity.ufp.com/identity-services/services/reenroll?name=slartibartfast&client_ip=192.0.2.42&type=delete'
      

If everything goes correctly, both reenroll operations would return an enrollment_context indicating success:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<enrollment_context>
  <name>slartibartfast</name>
  <result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="enrollmentResult" level="0" code="0" message="OK">SUCCESS</result>
</enrollment_context>
      

Batch enrollment

The UFP Identity service supports asynchronous enrolls. To support large numbers of existing users you can POST the existing users to the UFP Identity service in a specific format.

The format required mimics the enrollment operation with subtype import. To start, format the single element parameters, one per line ('\n'), prefixed with a '$' character.

$type=import
$client_ip=192.0.2.42
...
      

Next, append the header parameters that will be use to map the iteration over the values, prefixed by '$', comma separated, on one line ('\n'). e.g.

...
$name,$email,$password_hash
...
      

Finally, append the values, URL-encoded and comma separated, one per line ('\n') e.g.

...
slartibartfast,slartibartfast%40magrathea.com,e6e6854f6b71f43039d3994aca352307
fordprefect,fprefect%40betelgeuse.gov,9d40b3f333458234a3fee99bc7718fdb
arthurdent,arthur.dent%40gmail.com,0d79ec3cdf01f0d4897a8d2458bddebc
...
      

Assuming this data is written to a file named "enroll.txt" you can POST this file to the service.

echo -n "pass=`cat security.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathrea.key.pem \
-X POST -H"Content-Type: application/octet-stream" --data-binary @enroll.txt \
'https://identity.ufp.com/identity-services/services/enroll'
      

After POST'ing the data, you can check periodically to determine if the enroll is finished.

echo -n "pass=`cat security.key`" | curl -v -K - \
--cacert truststore.pem \
--cert magrathea.crt.pem \
--key magrathrea.key.pem \
'https://identity.ufp.com/identity-services/services/enroll/status'
      

Which either returns 200 OK if the enroll is finished, or 404 Not Found if the enroll is not finished yet.

During the period of time when asynchronous enrolls are being processed, existing user edits MUST be disabled. However, new user enrolls can still occur. The service typically handles about 125 enrolls/minute. If faster enrollment is needed or more than about 10K existing users need to be enrolled, please contact us.

Notes on batch enrollment.

A typical use case for large number of existing users is to batch enroll them. A library implementing batch enroll SHOULD allow a callback to actually write the iterable values to the outgoing stream. This allows you to page through large numbers of users without having to hold the entire user list in memory.