XMPP with Java Asmack library supporting X-FACEBOOK-PLATFORM

Finally, thanks to the no.good.at.coding code and the suggestion of harism, I’ve been able to connect to the Facebook chat. This code is the Mechanism for the Asmack library (the Smack port for Android). For the Smack library is necessary to use the no.good.at.coding mechanism.

SASLXFacebookPlatformMechanism.java:

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;

import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
import org.apache.harmony.javax.security.sasl.Sasl;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.util.Base64;

public class SASLXFacebookPlatformMechanism extends SASLMechanism
{

    private static final String NAME              = "X-FACEBOOK-PLATFORM";

    private String              apiKey            = "";
    private String              applicationSecret = "";
    private String              sessionKey        = "";

    /**
     * Constructor.
     */
    public SASLXFacebookPlatformMechanism(SASLAuthentication saslAuthentication)
    {
        super(saslAuthentication);
    }

    @Override
    protected void authenticate() throws IOException, XMPPException
    {

        getSASLAuthentication().send(new AuthMechanism(NAME, ""));
    }

    @Override
    public void authenticate(String apiKeyAndSessionKey, String host,
            String applicationSecret) throws IOException, XMPPException
    {
        if (apiKeyAndSessionKey == null || applicationSecret == null)
        {
            throw new IllegalArgumentException("Invalid parameters");
        }

        String[] keyArray = apiKeyAndSessionKey.split("\\|", 2);
        if (keyArray.length < 2)
        {
            throw new IllegalArgumentException(
                    "API key or session key is not present");
        }

        this.apiKey = keyArray[0];
        this.applicationSecret = applicationSecret;
        this.sessionKey = keyArray[1];

        this.authenticationId = sessionKey;
        this.password = applicationSecret;
        this.hostname = host;

        String[] mechanisms = { "DIGEST-MD5" };
        Map<String, String> props = new HashMap<String, String>();
        this.sc =
                Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
                        this);
        authenticate();
    }

    @Override
    public void authenticate(String username, String host, CallbackHandler cbh)
            throws IOException, XMPPException
    {
        String[] mechanisms = { "DIGEST-MD5" };
        Map<String, String> props = new HashMap<String, String>();
        this.sc =
                Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
                        cbh);
        authenticate();
    }

    @Override
    protected String getName()
    {
        return NAME;
    }

    @Override
    public void challengeReceived(String challenge) throws IOException
    {
        byte[] response = null;

        if (challenge != null)
        {
            String decodedChallenge = new String(Base64.decode(challenge));
            Map<String, String> parameters = getQueryMap(decodedChallenge);

            String version = "1.0";
            String nonce = parameters.get("nonce");
            String method = parameters.get("method");

            long callId = new GregorianCalendar().getTimeInMillis();

            String sig =
                    "api_key=" + apiKey + "call_id=" + callId + "method="
                            + method + "nonce=" + nonce + "session_key="
                            + sessionKey + "v=" + version + applicationSecret;

            try
            {
                sig = md5(sig);
            } catch (NoSuchAlgorithmException e)
            {
                throw new IllegalStateException(e);
            }

            String composedResponse =
                    "api_key=" + URLEncoder.encode(apiKey, "utf-8")
                            + "&call_id=" + callId + "&method="
                            + URLEncoder.encode(method, "utf-8") + "&nonce="
                            + URLEncoder.encode(nonce, "utf-8")
                            + "&session_key="
                            + URLEncoder.encode(sessionKey, "utf-8") + "&v="
                            + URLEncoder.encode(version, "utf-8") + "&sig="
                            + URLEncoder.encode(sig, "utf-8");

            response = composedResponse.getBytes("utf-8");
        }

        String authenticationText = "";

        if (response != null)
        {
            authenticationText =
                    Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
        }

        // Send the authentication to the server
        getSASLAuthentication().send(new Response(authenticationText));
    }

    private Map<String, String> getQueryMap(String query)
    {
        Map<String, String> map = new HashMap<String, String>();
        String[] params = query.split("\\&");

        for (String param : params)
        {
            String[] fields = param.split("=", 2);
            map.put(fields[0], (fields.length > 1 ? fields[1] : null));
        }

        return map;
    }

    private String md5(String text) throws NoSuchAlgorithmException,
            UnsupportedEncodingException
    {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(text.getBytes("utf-8"), 0, text.length());
        return convertToHex(md.digest());
    }

    private String convertToHex(byte[] data)
    {
        StringBuilder buf = new StringBuilder();
        int len = data.length;

        for (int i = 0; i < len; i++)
        {
            int halfByte = (data[i] >>> 4) & 0xF;
            int twoHalfs = 0;

            do
            {
                if (0 <= halfByte && halfByte <= 9)
                {
                    buf.append((char) ('0' + halfByte));
                }
                else
                {
                    buf.append((char) ('a' + halfByte - 10));
                }
                halfByte = data[i] & 0xF;
            } while (twoHalfs++ < 1);
        }

        return buf.toString();
    }
}

To use it:

ConnectionConfiguration config = new ConnectionConfiguration("chat.facebook.com", 5222);
config.setSASLAuthenticationEnabled(true);
XMPPConnection xmpp = new XMPPConnection(config);
try
{
    SASLAuthentication.registerSASLMechanism("X-FACEBOOK-PLATFORM", SASLXFacebookPlatformMechanism.class);
    SASLAuthentication.supportSASLMechanism("X-FACEBOOK-PLATFORM", 0);
    xmpp.connect();
    xmpp.login(apiKey + "|" + sessionKey, sessionSecret, "Application");
} catch (XMPPException e)
{
    xmpp.disconnect();
    e.printStackTrace();
}

apiKey is the API key given in the application settings page in Facebook. sessionKey is the second part of the access token. If the token is in this form, AAA|BBB|CCC, the BBB is the session key. sessionSecret is obtained using the old REST API with the method auth.promoteSession. To use it, it’s needed to make a Http get to this url:

https://api.facebook.com/method/auth.promoteSession?access_token=yourAccessToken

Despite of the Facebook Chat documentation says that it’s needed to use your application secret key, only when I used the key that returned that REST method I was able to make it works. To make that method works, you have to disable the Disable Deprecated Auth Methods option in the Advance tab in your application settings.

Leave a Comment