This product is currently in a closed beta.

Don't worry, you can still try it out today. Follow the instructions in this guide to build your integration. You can create and manage bug bounty test users here which you can use to immediately test Delegated Account Recovery with Facebook for your site.

Once you've finished building and testing your integration, you can apply for our closed beta program to enable all accounts to use Delegated Account Recovery with your site. As part of the review process for the closed beta, the Facebook Identity Tools team may contact you to collaborate on topics including security, customer messaging and the use of the Facebook brand in your deployment.

Delegated Account Recovery Step-by-Step Guide

There are a few basic technical tasks necessary to integrate with Delegated Account Recovery.

  1. Publish your service's configuration
  2. Fetch Facebook's configuration
  3. Create a recovery token for your user
  4. Send the token to Facebook
  5. Recover with a countersigned token

This guide will walk through these tasks step-by-step. Most of these tasks involve work on the server side of a web application. The instructions will assume you are using Heroku to deploy and have a bash command line environment available with the Heroku toolbelt and the openssl, curl, and perl packages installed. Where tasks require responding to HTTP requests from a web server, example code is shown for Node.js using the Express web framework and Java using the Spark web framework.

At the end of this guide, a number of advanced features will be discussed.

  1. Providing a nickname hint
  2. Getting token status callbacks
  3. Obsoleting a token
  4. Express Enrollment

SDK installation

Facebook's open source reference implementation of Delegated Account Recovery, including SDKs and example applications, is located at: https://github.com/facebook/DelegatedRecoveryReferenceImplementation or you can use standard package managers to add the SDK dependencies to your application.

You may choose any technology to implement your server. The Node.js and Java implementations discussed below are only illustrative examples. There is no requirement to use the Facebook-provided SDKs. You can implement your own in another language, such as Python or PHP, using Facebook's open source code as a guide, or based directly on the specification.

GitHub has published their independently developed Delegated Account Recovery SDK and example application in Ruby at https://github.com/github/darrrr

JavaScript / NodeJS

Run the following command:

$ npm install delegated-account-recovery

Or add the following to the dependencies section of your application's package.json:

"dependencies": {
    ...
    "delegated-account-recovery": ">= 1.0.2",
    ...
},

Java

Add the following dependency to your pom.xml:

<dependencies>
  ...
  <dependency>
    <groupId>com.facebook.delegatedrecovery</groupId>
    <artifactId>delegatedrecovery-sdk</artifactId>
    <version>1.0.1</version>
  </dependency>
  ...
</dependencies>

Publish Your Service's Configuration

Web sites can participate in Delegated Account Recovery without needing to sign up for an app ID, pre-configure URLs, or receive an app secret from Facebook. All of the necessary information to participate in the protocol is published at a standard location over HTTPS. The configuration includes a few URLs for the resources used in the protocol, an identity statement for your service, and the public keys that are used to validate tokens.

The configuration location is always in a well-known location on your domain. For example.com that location would be:

https://example.com/.well-known/delegated-account-recovery/configuration

The /.well-known/ path prefix is defined by RFC 5785 as a standard place to place site-wide configuration information. You should make sure that only administratively privileged users can publish under this path on your domains.

Here is an example of the data https://example.com might publish:

{
    "issuer": "https://example.com",
    "tokensign-pubkeys-secp256r1": [
        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Oiop2rS8RXFxxrUXLTQUlKllQo7yEUVyQzu8L+Cxh2SOI7G75yVAy9PYtMCf4mJWv+IkhaFQscKbR2JcZ02iA=="
    ],
    "save-token-return": "https://example.com/saveTokenReturn",
    "recover-account-return": "https://example.com/recoverAccountCallback",
    "privacy-policy": "https://example.com/privacy",
    "icon-152px": "https://example.com/icon.png"
}

The requests to fetch configuration will be made by servers, not browsers. If your web framework blocks requests based on the User-Agent header, or has other "anti-scraping" controls, you should disable those checks for the configuration URL so it can be read from anywhere.

The issuer field describes the identity of your service, expressed as an ASCII-serialized Origin, (RFC 6454) that is, the https:// scheme, followed by the Fully Qualified Domain Name, encoded as ASCII or punycode, with no trailing /.

The tokensign-pubkeys-secp256r1 array contains the current public keys used to sign your recover tokens.

icon-152px is an optional field pointing to a 152x152 pixel PNG icon to represent the issuer.

The tokensign-pubkeys-secp256r1 array contains the current public signing keys used when a token is being saved.

The remaining fields describe the protocol endpoints. save-token-return is the URL that the Facebook will redirect users to after they save a token (successfully, or on failure), and recover-account-return is where the countersigned token will be POSTed during the recovery process.

The Delegated Account Recovery protocol relies on cross-origin HTTP POSTs to send tokens with less risk of accidental leaks through redirects, referrer headers, and other risks of GET. If your application uses CSRF protection tokens to protect all POST requests, you must disable that protection for the recover-account-return endpoint.

Finally there is privacy-policy. While not used directly in the protocol, this is a mandatory field, describing where people and services can read the service's privacy policy.

Generating a token signing key pair

You SHOULD rotate your token signing keys periodically. If you follow the advice in this guide to save the recovery provider's origin, the token id, a hash of the token and a reference to the user object to which it belongs, you will not need your previous private keys to validate tokens you have issued in the past, and it is safe to rotate your keys at any time.

If you choose not to save this per-user state at your server, you MUST remember all previous public keys used to sign tokens so you can verify they have not been forged or tampered with when they are used for a recovery.

If you choose to save data in a token, (see Advanced Topics) you will need an additional symmetric encryption key to protect that data, and you MUST not lose it or you will be unable to recover the data.

The following openssl commands will generate a public/private key pair on the NIST P-256 elliptic curve:

$ openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-key.pem
$ openssl ec -in prime256v1-key.pem -pubout -out prime256v1-pub.pem

The contents of prime256v1-pub.pem will be published. Both the public and private keys will need to be accessible to the application, but you should be careful not to disclose the private key. Keep it safe, but do not check it into your source control system, for example.

If you are deploying one of our example apps on Heroku, you will need to do the following to publish your configuration:

The following commands will inject the keys into your Heroku application config in the format expected by the sample applications. (stripped of headers and joined into a single line) Follow the instructions appropriate for your own application environment if deploying to a different platform.

$ heroku config:set --app YOUR_APP_NAME RECOVERY_PRIVATE_KEY=`perl -p -e 's/\R//g; s/-----[\w\s]+-----//' prime256v1-key.pem`
$ heroku config:set --app YOUR_APP_NAME RECOVERY_PUBLIC_KEY=`perl -p -e 's/\R//g; s/-----[\w\s]+-----//' prime256v1-pub.pem`

Then, you will need to configure what origin to publish for your service. You can do this in Herkou for our example apps as follows:

$ heroku config:set --app [your-app-name] ISSUER_ORIGIN="https://example.herokuapp.com"

Node.js example

To publish an Account Provider configuration, use the delegated-recovery-account-provider module as Express middleware.

Configure the middleware in your application's main file, which will automatically publish at the well-known location.

const delegatedRecoveryUtils = require('./local/delegated-recovery-sdk/index.js');

// Heroku-specific application configuration
const recoveryPubKey = process.env.RECOVERY_PUBLIC_KEY;
const issuerOrigin = process.env.ISSUER_ORIGIN;

app.use(delegatedRecoveryUtils.middleware({
    'issuer' : issuerOrigin,
    'save-token-return': path.web.saveTokenReturn,
    'recover-account-return': path.web.recoverAccountReturn,
    'privacy-policy': path.web.privacyPolicy,
    'publicKeys': [recoveryPubKey],
    'icon-152px': path.web.icon,
    'config-max-age': 600 // 10 minutes
}));  

There are other option keys necessary to finish configuring the middleware. For clarity, these additional options will be discussed in following steps, as they are explained.

Java example

The example app shows how to publish an Account Provider configuration in the Spark framework:

import static spark.Spark.*;
import com.facebook.delegatedrecovery.*;
...

String issuerOrigin = new ProcessBuilder().environment().get("ISSUER_ORIGIN");
String publicKey    = new ProcessBuilder().environment().get("RECOVERY_PUBLIC_KEY");
...

AccountProviderConfiguration accountProviderConfig = 
    new AccountProviderConfiguration(
        issuerOrigin,
        issuerOrigin + Path.Web.SAVE_TOKEN_RETURN,
        issuerOrigin + Path.Web.RECOVER_ACCOUNT_RETURN,
        issuerOrigin + Path.Web.PRIVACY_POLICY,
        new String[] { publicKey },
        issuerOrigin + Path.Web.ICON);

get(DelegatedRecoveryConfiguration.CONFIG_PATH, "application/json", (req, res) -> {
    res.header("Cache-Control", "max-age=60");
    return accountProviderConfig.toString();
});


Fetch Facebook's Configuration

Now that you've published your service's configuration where Facebook can read it, next you need to retrieve Facebook's configuration.

The same well-known URL is used, for "https://www.facebook.com"

https://www.facebook.com/.well-known/delegated-account-recovery/configuration

To retrieve the configuration and examine it from the command line:

$ curl https://www.facebook.com/.well-known/delegated-account-recovery/configuration
{
    "issuer": "https://www.facebook.com",
    "countersign-pubkeys-secp256r1": [
        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk8cg3smrHU1zr3UUNQIDeM4wL692B1EhN5nBhw9F5/OXZC6VijikyqKHtAU/zyiEy/bXPiv7DV/JpzoD8Imlew==",
        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEriQNK6MFwepmp+oXcRrbC3KHLuUPiBEswly65lEPznI/UQwm0gFNHhqEyCKAt134uqAaICqyuTwmEdLaXc+hXg=="
    ],
    "token-max-size": 8192,
    "icon-152px": "/apple-touch-icon.png"
    "save-token": "https://www.facebook.com/recovery/delegated/save",
    "save-token-async-api-iframe": "https://www.facebook.com/plugins/delegated_account_recovery",
    "recover-account": "https://www.facebook.com/recovery/delegated/recover",
    "privacy-policy": "https://www.facebook.com/about/privacy/",
}

This is an example configuration - these values will change over time. You always need to fetch a fresh configuration and keep it no longer than the Cache-Control header in the response specifies.

The issuer, privacy-policy, and icon-152px fields are the same use as for your service's configuration.

token-max-size is an optional field that describes the maximum size recovery token this provider is willing to save. If you are going to use the data field of a recovery token, you should verify that it is not too big to save.

countersign-pubkeys-secp256r1 is an array of public keys used to verify countersigned tokens sent by the service as part of the recovery process.

The remaining fields describe the protocol endpoints. save-token is an endpoint to which a recovery token can be POSTed to save it and recover-account is the endpoint where your service directs a user's browser to start the recovery process.

(save-token-async-api-iframe is discussed under advanced features)

Successfully fetching a correctly formed configuration is an indication that the domain supports the Delegated Account Recovery protocol. When saving a token, the configuration will tell you where to send it. When doing a recovery, it will tell you where to send the user and the keys for validating the signature on the countersigned token that is returned.

Node.js example

To retrieve a configuration in our Node.js SDK, use fetchConfiguration, which returns a Promise:

const delegatedRecoveryUtils = require('./local/delegated-recovery-sdk/index.js');

delegatedRecoveryUtils.fetchConfiguration(recoveryProvider).then(
    (config) => {
        // do what you need to with the configuration (probably cache it)
    },
    (e) => {
        // handle error condition
    });
}

Java example

To retrieve a configuration in our Java SDK, use DelegatedRecoveryAccountProvider.fetchConfiguration, which returns a DelegatedAccountRecoveryConfiguration object that can be cast to a RecoveryProviderConfiguration.

import com.facebook.delegatedrecovery.*;
import com.facebook.delegatedrecovery.DelegatedRecoveryAccountProvider.ConfigType;
...

try {
    RecoveryProviderConfiguration config = (RecoveryProviderConfiguration)
        DelegatedRecoveryAccountProvider.fetchConfiguration(
            "https://www.facebook.com", 
            ConfigType.RECOVERY_PROVIDER
        );
    // success
} catch (Exception e) {
    // handle error condition
    System.err.println(e.getMessage());
}


Create a Recovery Token for Your User

Now that you have fetched the configuration for Facebook and published your configuration as an Account Provider, you are ready to create a recovery token for a user.

Recovery tokens are saved at Facebook and used later by your service to re-identify the user. You need to make a decision about how to do that re-identification, and what to save in the token. The simplest choice is to save no data in the token. Saving data in a token is for advanced use cases and requires more complicated key management. It is much easier to simply record the token ID and a SHA-256 hash of the token with the user's account data at your service. This is enough to prove the authenticity and association to the correct user of a token sent back from Facebook, with less cryptographic code to write and less key management overhead.

Node.js example

The following code creates a recovery token with no data and builds an object that can be saved with the local user account to recognize the token later.

const delegatedRecoveryUtils = require('./local/delegated-recovery-sdk/index.js');
const crypto = require('crypto');
...

// Heroku-specific application configuration
const recoveryPrivKey = process.env.RECOVERY_PRIVATE_KEY;

// config is the configuration for the Recovery Provider from the first step of this guide
const id = crypto.randomBytes(16);
const token = new RecoveryToken(
    recoveryPrivKey,
    id,
    RecoveryToken.STATUS_REQUESTED_FLAG, // get status callbacks
    issuerOrigin,
    config.issuer,
    new Date().toISOString(),
    Buffer.alloc(0),  // no data in this token
    Buffer.alloc(0)); // no token binding

// record keeping: save that this token was created and is in a provisional
// state until we know it has been saved successfully at the recovery provider
tokenRecords.push({
    status: recordStatus.provisional,
    username: username,
    id: id.toString('hex'),
    issuer: config.issuer,
    hash: delegatedRecoveryUtils.sha256(new Buffer(token.encoded, 'base64'))
});

// put the token into the model and return a view
res.render(path.template.saveToken, {
    "encoded-token": token.encoded,
    "username": username,
    "state": id.toString('hex'),
    "save-token": config['save-token']
});

Java example

import com.facebook.delegatedrecovery.DelegatedRecoveryUtils;
import com.facebook.delegatedrecovery.RecoveryToken;
...  

byte[] id = DelegatedRecoveryUtils.newTokenID();
String stringID = DelegatedRecoveryUtils.encodeHex(id);

RecoveryToken token = new RecoveryToken(privateKey, // signing key
    id, 
    RecoveryToken.STATUS_REQUESTED_FLAG, // get lifecycle callbacks
    Main.getAccountProviderConfig().getIssuer(), // our origin
    Main.getRecoveryProviderConfig().getIssuer(), // origin from Facebook's config
    new byte[0], // no data
    new byte[0]); // no binding

// record keeping: save that this token was created and is in a provisional
// state until we know it has been saved successfully at the recovery provider
RecoveryTokenRecordDao.addRecord(new RecoveryTokenRecord(
    username,
    stringID,
    token.getAudience(),
    DelegatedRecoveryUtils.sha256(Base64.getDecoder().decode(encoded)),
    RecoveryTokenRecord.Status.PROVISIONAL));  

// put the token into the model and return a view
model.put("encoded-token", token.getEncoded());
model.put("username", username);
model.put("state", stringID);
model.put("save-token", Main.getRecoveryProviderConfig().getSaveToken());

return Main.render(model, Path.Template.SAVE_TOKEN);

Send the Token to Facebook

Now that you have created a recovery token and saved the relevant information with the user object in your system, you have to send it to the Facebook. This is why the notes in the previous section indicate that the status of the token should be marked as "PENDING". You don't know yet if the user and Facebook will accept saving it.

The simplest way to send the token is to instruct the user's browser to POST it to the save-token endpoint for Facebook using HTML like the following. You can optionally add a state parameter which Facebook will send back to you. This parameter should not contain confidential information as it may be passed in the query string. This example will use the token hash as state.

<html>
  <body>
    <div id="message">
       You don't have a way to recover if you forget your password!
    </div>
    <form method="POST" action="{{save-token}}">
      <input type="hidden" name="token" value="{{encoded-token}}">
      <input type="hidden" name="state" value="{{state}}">
      <input type="hidden" name="nickname_hint" value="{{username}}">
      <input class="button" type="submit" value="Use Facebook">
    </form>
  </body>
</html>

At Facebook, the user will log in (if necessary) and accept saving the token to their account. In the process of doing this, Facebook will fetch your service's configuration and use it to validate the token signature, present your domain and icon as part of the consent experience, and determine where to send the user after they have either completed saving the token, or declined to.

If the user successfully saved the token, your service will receive a request from the user's browser like:

https://example.herokuapp.com/saveTokenReturn?status=save-success&state={{state}}

If for any reason, the save operation was unsuccessful, you will receive a request like:

https://example.herokuapp.com/saveTokenReturn?status=save-failure&state={{state}}

It is up to your application to decide exactly how to handle these events.

Node.js example

app.get(path.web.saveTokenReturn, (req, res) => {
    const id = req.query.state;
    // find and update our pending token record to confirmed status
    const tokenRecord = tokenRecords.find((record) => record.id === id);
    if (tokenRecord === undefined) {
        res.render(path.template.unknownToken, {});
    } else if (req.query.status === 'save-success') {
        tokenRecord.status = recordStatus.confirmed;
        res.render(path.template.saveTokenSuccess, {
            username: tokenRecord.username
        });
    } else {
        // remove from list of pending tokens if failed to save
        tokenRecords.splice(tokenRecords.findIndex((record) => record.id === id), 1);
        res.render(path.template.saveTokenFailure, {
            username: tokenRecord.username,
            homeAction: path.web.saveToken
        });
    }
});

Java example

/**
 * Landing page when returning from saving a token at Facebook, updates the
 * local records of token status in the RecoveryTokenRecordDao
 */
public static Route serveSaveTokenReturn = (Request req, Response res) -> {
  Map<String, Object> model = new HashMap<String, Object>();
  String id = req.queryParams("state");
  // find and update our pending token record to confirmed status
  RecoveryTokenRecord record = RecoveryTokenRecordDao.getTokenRecordById(id);

  if (record == null) {
    return Main.render(model, Path.Template.UNKNOWN_TOKEN);
  }

  if (req.queryParams("status").equals("save-success")) {
    record.setStatus(RecoveryTokenRecord.Status.CONFIRMED);
    model.put("username", record.getUsername());
    return Main.render(model, Path.Template.SAVE_TOKEN_SUCCESS);
  } else {
    // remove from list of pending tokens if failed to save
    RecoveryTokenRecordDao.deleteRecordById(id);
    model.put("username", record.getUsername());
    model.put("homeAction", Path.Web.SAVE_TOKEN);
    return Main.render(model, Path.Template.SAVE_TOKEN_FAILURE);
  }
};

Recover with a Countersigned Token

Your user has saved a recovery token. Time passes, they forget their password or change their phone number, and lose access to your service. It is time to recover.

It is up to you to decide how your application will present the introduction to the recovery experience, including identifying their account.

A unique advantage of Delegated Account Recovery is that if your service only uses recovery at Facebook, or the user can identify Facebook as the service they use for recovery, you can send the user to Facebook's recovery endpoint without needing to identify their account at your service first. You can look it up based on the token data once it is returned.

When you are ready to get a countersigned recovery token for the user from Facebook, fetch Facebook's configuration as previously explained. Send the user's browser to the recover-account endpoint.

If you have saved per-user state and know the token ID for the user, you can add its hexidecimal encoded value as the GET parameter id. This will help Facebook select the correct token, in case the user has more than one token for your service saved at their account. You can also specify issuer as a GET parameter to help the Recovery Provider filter the tokens offered to the user to only those from your service, if you don't know the token ID.

When the user has satisfied the Facebook as to their identity, Facebook will fetch your service's configuration, wrap the user's recovery token for your service in a countersigned token, and instruct the user's browser to send it to your service's recover-account-return endpoint. The POST body parameter token will have the countersigned token as a base64 encoded string.

Token re-use considerations

A saved recovery token (issued by your service) is not invalidated at Facebook after a single use. This token persists until the user chooses to delete it and can be used any number of times. It is always your choice as an application developer to choose the conditions under which you will honor a countersigned token, but it is recommended that you allow the same token to be used more than once. If you want to replace a token after a single use, see obsoleting a token under "Advanced Features", below.

A countersigned recovery token (issued by Facebook) SHOULD only be used once. Your service should track, for at least the time window in which a token is valid, the ID or a hash of any countersigned tokens used for recovery and not allow them to be replayed.

Node.js

const replayCache = [];

app.post(path.web.recoverAccountReturn, (req, res) => {
    let errorFlag = false;

    const errorFunction = (message) => {
        errorFlag = true;
        res.render(path.template.recoverAccountFailure, {
            "exception": message
        });
    }

    const token = req.body.token;
    if (token === null || token === '') {
        errorFunction('No token.');
    }

    if (replayCache.find((item) => item === token) !== undefined) {
        errorFunction('Countersigned token replay detected!');
    } else {
        replayCache.push(token);
    }

    const issuer = delegatedRecoveryUtils.extractIssuer(token);

    // is multiple issuers were supported, would fetch config here, but
    // this sample app only uses Facebook with a statically cached config
    recoveryProviderConfig().then((config) => {
        if (issuer !== config.issuer) {
            errorFunction('Countersigned token issuer invalid: ' + issuer);
        }
        let countersignedToken = null;
        try {
            countersignedToken = CountersignedToken.fromSerialized(
                new Buffer(token, 'base64'),
                issuer,
                issuerOrigin,
                60 /*sec*/ * 60 /*min*/ , // 1 hour clock skew
                Buffer.alloc(0), // no token binding expected
                config['countersign-pubkeys-secp256r1']
            );
        } catch (e) {
            errorFunction(e);
        }

        if (countersignedToken !== null) {
            const innerHash = delegatedRecoveryUtils.sha256(countersignedToken.data);
            const expectedUsername = req.body.state;
            const record = tokenRecords.find((record) => record.hash === innerHash);

            if (record === undefined) {
                errorFunction('No record of this token. Perhaps you restarted this app since it was issued?');
            } else if (record.status !== recordStatus.confirmed) {
                errorFunction('The recovery token from this app wasn\'t marked as valid.');
            } else if (expectedUsername !== undefined && expectedUsername !== '' && expectedUsername !== record.username) {
                errorFunction('The recovery token from this app was not for ' + expectedUsername);
            }

            if (!errorFlag) {
                res.render(path.template.recoverAccountSuccess, {
                    username: record.username
                });
            }
        }
    }, (e) => {
        errorFunction(e);
    });
});

Java

/**
 * Handle an incoming countersigned recovery token and give access to account
 * if correct
 */
public static Route serveRecoverAccountReturn = (Request req, Response res) -> {
  try {
    String encoded = req.queryParams("token");
    if (encoded == null || encoded.equals("")) {
      throw new Exception("No recovery token.");
    }

    // check relay cache if we've seen this countersigned token before
    synchronized (replayCache) {
      if (replayCache.contains(encoded)) {
        throw new Exception("countersigned token replay detected!");
      } else {
        replayCache.add(encoded);
      }
    }

    String issuer = CountersignedRecoveryToken.extractIssuer(encoded);
    RecoveryProviderConfiguration recoveryProviderConfig = Main.getRecoveryProviderConfig();

    // if the token incoming isn't from Facebook, which we have cached, fetch
    // the correct configuration
    if (!issuer.equals(recoveryProviderConfig.getIssuer())) {
      recoveryProviderConfig = (RecoveryProviderConfiguration) DelegatedRecoveryUtils.fetchConfiguration(issuer,
          DelegatedRecoveryConfiguration.ConfigType.RECOVERY_PROVIDER);
    }

    // constructing a countersigned token automatically validates the outer
    // token
    CountersignedRecoveryToken countersignedToken = new CountersignedRecoveryToken(encoded, issuer,
        Main.getAccountProviderConfig().getIssuer(), // our service's issuer
                                                     // is audience for
                                                     // countersigned token
        recoveryProviderConfig.getPubKeys(), 60 * 60,// validity period in
                                                     // seconds (one hour)
        null                                         // no token binding expected
    );

    RecoveryTokenRecord record = RecoveryTokenRecordDao.getTokenRecordByHash(countersignedToken.getInnerTokenHash());
    String expectedUsername = req.queryParams("state");

    if (record == null) {
      throw new Exception("No record of this token. Perhaps you restarted this app since it was issued?");
    } else if (record.getStatus() != RecoveryTokenRecord.Status.CONFIRMED) {
      throw new Exception("The recovery token from this app wasn't market as valid.");
    } else if (expectedUsername != null && !expectedUsername.equals("")
        && !expectedUsername.equals(record.getUsername())) {
      throw new Exception("The recovery token from this app was not for " + expectedUsername);
    } else {
      Map<String, Object> model = new HashMap<String, Object>();
      model.put("username", record.getUsername());
      return Main.render(model, Path.Template.RECOVER_ACCOUNT_SUCCESS);
    }
  } catch (Exception e) {
    Map<String, Object> model = new HashMap<String, Object>();
    model.put("exception", e.getMessage());
    StringWriter sw = new StringWriter();
    e.printStackTrace(new PrintWriter(sw));
    model.put("stackTrace", sw.toString());
    return Main.render(model, Path.Template.RECOVER_ACCOUNT_FAILURE);
  }
};

Advanced Features

Providing a nickname hint

When saving a token, addition to the token parameter, you can include the nickname_hint parameter. This parameter allows you to provide a hint about what account the recovery token refers to. This might be a symbolic name, like "Home Account" or "Work Account", or it might be a masked version of the username or a contact point. Users can always change the nicknames associated with their saved tokens, and the nickname is not returned to the account provider during a recovery. Nicknames are just to make it easier for people to manage multiple tokens from the same account provider.

Getting token status callbacks

It can be useful to request token status if you keep track of token IDs, but token status reporting is an OPTIONAL feature for Recovery Providers, and reliable delivery is not guaranteed. Facebook will attempt to report token status but will not retry if it cannot be delivered. Use status updates to enhance your user experience but do not count on them.

A common problem with using email or SMS based recovery mechanisms is that when a person changes their email address or phone number, it is a manual process to update that information on all of their accounts. Often, they may not realize that their recovery information is out of date until they need it. Delegated Account Recovery tries to improve on this by offering token status callbacks. If you set the low bit of the token "options" byte, it indicates that you wish to receive these callbacks.

A callback is an HTTP POST to a well-known endpoint. If your issuer is https://example.com, that would be:

https://example.com/.well-known/delegated-account-recovery/token-status

The token status endpoint is POSTed to by the Recovery Provider's server. You must disable any CSRF protections on that path to receive status calls.

The POST body contains two parameters. id is the hex-encoded token identifier to which the event pertains, and status is one of the following values:

  • save-success reports that a token has been successfully saved with the Recovery Provider.
  • save-failure reports that a token was sent to the Recovery Provider but was not saved.
  • deleted reports that the user has deleted the token at the Recovery Provider. The Account Provider may want to prompt the user to establish a new account recovery capability if the deleted token was the only one associated with the account.
  • token-repudiated reports that a user has informed the Recovery Provider that a token was associated with their account without their consent.
  • recovery-repudiated reports that a user has informed the Recovery Provider that a recovery action was initiated with a token without their consent.

It is up to your application to decide what action to take in response to receiving one of these status events.

Node.js

// tokenRecords is the toy token state management for the sample application
app.post(delegatedRecoveryUtils.STATUS_PATH, (req, res) => {
    const id = req.body.id;
    const tokenRecord = tokenRecords.find((record) => record.id === id);
    if (tokenRecord !== undefined) {
        switch (req.body.status) {
        case 'save-success':
            tokenRecord.status = recordStatus.confirmed;
            break;
        case 'save-failure':
            tokenRecords.splice(tokenRecords.findIndex((record) => record.id === id), 1);
            break;
        case 'token-repudiated':
            tokenRecord.status = recordStatus.invalid;
            break;
        }
    }
    res.status(200).send();
});

Java

// RecoveryTokenRecordDao is the toy token state management for the sample application
post(DelegatedRecoveryConfiguration.TOKEN_STATUS_PATH, (req, res) -> {
      String id = req.queryParams("id");
      String status = req.queryParams("status");

      RecoveryTokenRecord record = RecoveryTokenRecordDao.getTokenRecordById(req.queryParams("id"));

      if (record != null && status != null) {
        if (status.equals("save-success")) {
          record.setStatus(RecoveryTokenRecord.Status.CONFIRMED);
        } else if (status.equals("save-failure") || status.equals("deleted")) {
          RecoveryTokenRecordDao.deleteRecordById(id);
        } else if (status.equals("token-repudiated")) {
          record.setStatus(RecoveryTokenRecord.Status.INVALID);
        }
      }
      res.status(200);
      return "";
    });

Obsoleting a token

Tokens stay attached to a person's Facebook account until they choose to delete them. Although a countersigned token issued by Facebook is intended to be single-use, it is intended that the recovery tokens issued by your service and saved at a Facebook account can be used many times. The recovery token saved to the account represents a long-term connection, and the counter-signed tokens represent the point-in-time re-authentication event.

Because these tokens are data belonging to the account owner, your service cannot remotely delete them, though it is always at your service's discretion whether it wants to continue to trust a saved token.

Your service can, with the owner's consent, replace an obsolete token with a new one. If you want to rotate a token after it is used, or if you have lost trust in it for any other reason (perhaps your token signing key was compromised) you can send a new token to replace it. Add an obsoletes HTTP parameter to the request to the save-token endpoint with the hex-encoded value of the id of the token you want to replace. If Facebook sees a token attached to that user's account, from your origin, with the indicated id, it will mark it as obsolete and replace it with the newly validated token if the person agrees.

Express Enrollment

Express Enrollment streamlines the experience of setting up account recovery with Facebook. If we are able to match the logged in Facebook account to your application's notion of the user's identity, we can complete the enrollment process without an additional confirmation screen on Facebook.

To use Express Enrollment, you must pass an indication of the email or phone number contact point you have for the account in your application. You can provide it directly or as a salted, one-way cryptographic hash.

To provide the hint directly, include a login_hint parameter with your call to the save-token endpoint, with the value set to either the email address or phone number for the account.

To provide a hashed hint, include a login_hint_sha256 parameter. The value of this parameter should be obtained as follows:

  1. Create a salt value containing 32 bytes of random data (e.g. with urandom(32))
  2. Represent the contact point email or phone number as an ASCII octet string.
  3. Concatenate the octet string representing the random salt from step 1 to the octet string representing the contact point from step 2.
  4. Obtain a new 32 byte octet string by applying the SHA-256 algorithm to the result of step 3.
  5. Obtain a new 64 byte octet string by concatenating the salt from step 1 with the results of step 4.
  6. Base64 encode the results of step 5.

If using a phone number contact point, we recommend passing it as the login_hint parameter, as there are many variations in how international numbers are represented. Facebook can match across most formats with an un-hashed hint, but any differences in format will prevent a match if using login_hint_sha256.

If Express Enrollment is enabled for your domain, when receiving a save-token invocation containing a login_hint or login_hint_sha256 parameter, Facebook will attempt to match it with the confirmed contact points for the currently logged in account.

If a match is found, the recovery token will be saved and the person immediately redirected to your save-token-return endpoint. The person will see a notification that is enabled for your application the next time they visit their Facebook account.

If no match is found, the person will be see a screen from Facebook prompting them to confirm saving the token, as usual.

Use of the Express Enrollment feature must be enabled for your application and requires that you provide notice and consent (such as a checkbox or button with the Facebook name or logo) on your site that Facebook will be used for recovery.

Facebook will rate limit how often it attempts to match contact point hints for a given account. You should not attempt Express Enrollment more than once per account.

Contact your Facebook partnership manager to enable Express Enrollment for your application.