aboutsummaryrefslogtreecommitdiffstats
path: root/facebook/facebook-api.c
diff options
context:
space:
mode:
authordequis <dx@dxzone.com.ar>2017-10-16 22:11:42 -0300
committerdequis <dx@dxzone.com.ar>2017-10-16 22:11:42 -0300
commit24db488909604dd389b584c1f1ce43c549648dbe (patch)
treecd5bb0df1a81a8ed370540c800eb13bbb5051f3a /facebook/facebook-api.c
parent1e264442a48f33a5933c49a2c0332e426dcdb4a1 (diff)
downloadbitlbee-facebook-24db488909604dd389b584c1f1ce43c549648dbe.tar.gz
bitlbee-facebook-24db488909604dd389b584c1f1ce43c549648dbe.tar.bz2
bitlbee-facebook-24db488909604dd389b584c1f1ce43c549648dbe.tar.xz
Work chat login support (enable the "work" setting to use it)
This covers three autodetected login types: 1. Work account password Simple, very similar to normal account types 2. Linked personal account This is deprecated but still needed in some companies. Looks just like password auth to users. In rare cases there may be more than one work account linked to a personal account, in which case this will only use the first one. Usually they can be de-linked by assigning a password (see the official docs) 3. SSO This one is awkward. The password can be set to garbage and users will receive a PM with instructions to do an oauth-like login, but there's no explicit auth code screen, just a redirect to a fb-workchat-sso://, which probably results in an error. Users are expected to copy that url, hopefully from the address bar. Not very practical, but works! In all cases, the username is the work account email.
Diffstat (limited to 'facebook/facebook-api.c')
-rw-r--r--facebook/facebook-api.c280
1 files changed, 273 insertions, 7 deletions
diff --git a/facebook/facebook-api.c b/facebook/facebook-api.c
index 6783ba4..27a8aab 100644
--- a/facebook/facebook-api.c
+++ b/facebook/facebook-api.c
@@ -27,6 +27,7 @@
#include "facebook-util.h"
typedef struct _FbApiData FbApiData;
+typedef struct _FbApiPreloginData FbApiPreloginData;
enum
{
@@ -64,6 +65,10 @@ struct _FbApiPrivate
FbId lastmid;
gchar *contacts_delta;
int tweak;
+ gboolean is_work;
+ gboolean need_work_switch;
+ gchar *sso_verifier;
+ FbId work_community_id;
};
struct _FbApiData
@@ -72,6 +77,13 @@ struct _FbApiData
GDestroyNotify func;
};
+struct _FbApiPreloginData
+{
+ FbApi *api;
+ gchar *user;
+ gchar *pass;
+};
+
static void
fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg);
@@ -209,6 +221,7 @@ fb_api_dispose(GObject *obj)
g_free(priv->stoken);
g_free(priv->token);
g_free(priv->contacts_delta);
+ g_free(priv->sso_verifier);
}
static void
@@ -546,6 +559,22 @@ fb_api_class_init(FbApiClass *klass)
fb_marshal_VOID__POINTER,
G_TYPE_NONE,
1, G_TYPE_POINTER);
+
+ /**
+ * FbApi::work-sso-login:
+ * @api: The #FbApi.
+ *
+ * Emitted when user interaction is required to continue SAML SSO login
+ */
+
+ g_signal_new("work-sso-login",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL,
+ fb_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
}
static void
@@ -764,7 +793,8 @@ fb_api_http_req(FbApi *api, const gchar *url, const gchar *name,
GList *l;
GString *gstr;
- fb_http_values_set_str(values, "api_key", FB_API_KEY);
+ fb_http_values_set_str(values, "api_key",
+ priv->is_work ? FB_WORK_API_KEY : FB_API_KEY);
fb_http_values_set_str(values, "device_id", priv->did);
fb_http_values_set_str(values, "fb_api_req_friendly_name", name);
fb_http_values_set_str(values, "format", "json");
@@ -787,7 +817,7 @@ fb_api_http_req(FbApi *api, const gchar *url, const gchar *name,
g_string_append_printf(gstr, "%s=%s", key, val);
}
- g_string_append(gstr, FB_API_SECRET);
+ g_string_append(gstr, priv->is_work ? FB_WORK_API_SECRET : FB_API_SECRET);
data = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str, gstr->len);
fb_http_values_set_str(values, "sig", data);
@@ -2109,6 +2139,53 @@ fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg)
}
static void
+fb_api_cb_work_peek(FbHttpRequest *req, gpointer data)
+{
+ FbApi *api = data;
+ FbApiPrivate *priv = api->priv;
+ GError *err = NULL;
+ JsonNode *root;
+ gchar *community = NULL;
+
+ if (!fb_api_http_chk(api, req, &root)) {
+ return;
+ }
+
+ /* The work_users[0] explicitly only handles the first user.
+ * If more than one user is ever needed, this is what you want to change,
+ * but as far as I know this feature (linked work accounts) is deprecated
+ * and most users can detach their work accounts from their personal
+ * accounts by assigning a password to the work account. */
+ community = fb_json_node_get_str(root,
+ "$.data.viewer.work_users[0].community.login_identifier", &err);
+
+ FB_API_ERROR_EMIT(api, err,
+ g_free(community);
+ json_node_free(root);
+ return;
+ );
+
+ priv->work_community_id = FB_ID_FROM_STR(community);
+
+ fb_api_auth(api, "X", "X", "personal_to_work_switch");
+
+ g_free(community);
+ json_node_free(root);
+}
+
+static FbHttpRequest *
+fb_api_work_peek(FbApi *api)
+{
+ FbHttpValues *prms;
+
+ prms = fb_http_values_new();
+ fb_http_values_set_int(prms, "doc_id", FB_API_WORK_COMMUNITY_PEEK);
+
+ return fb_api_http_req(api, FB_API_URL_GQL, "WorkCommunityPeekQuery",
+ "post", prms, fb_api_cb_work_peek);
+}
+
+static void
fb_api_cb_auth(FbHttpRequest *req, gpointer data)
{
FbApi *api = data;
@@ -2123,7 +2200,14 @@ fb_api_cb_auth(FbHttpRequest *req, gpointer data)
values = fb_json_values_new(root);
fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.access_token");
- fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid");
+
+ /* extremely silly difference */
+ if (priv->is_work) {
+ fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.uid");
+ } else {
+ fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid");
+ }
+
fb_json_values_update(values, &err);
FB_API_ERROR_EMIT(api, err,
@@ -2134,25 +2218,202 @@ fb_api_cb_auth(FbHttpRequest *req, gpointer data)
g_free(priv->token);
priv->token = fb_json_values_next_str_dup(values, NULL);
- priv->uid = fb_json_values_next_int(values, 0);
- g_signal_emit_by_name(api, "auth");
+ if (priv->is_work) {
+ priv->uid = FB_ID_FROM_STR(fb_json_values_next_str(values, "0"));
+ } else {
+ priv->uid = fb_json_values_next_int(values, 0);
+ }
+
+ if (priv->need_work_switch) {
+ fb_api_work_peek(api);
+ priv->need_work_switch = FALSE;
+ } else {
+ g_signal_emit_by_name(api, "auth");
+ }
+
g_object_unref(values);
json_node_free(root);
}
void
-fb_api_auth(FbApi *api, const gchar *user, const gchar *pass)
+fb_api_auth(FbApi *api, const gchar *user, const gchar *pass, const gchar *credentials_type)
{
+ FbApiPrivate *priv = api->priv;
FbHttpValues *prms;
prms = fb_http_values_new();
fb_http_values_set_str(prms, "email", user);
fb_http_values_set_str(prms, "password", pass);
+
+ if (credentials_type) {
+ fb_http_values_set_str(prms, "credentials_type", credentials_type);
+ }
+
+ if (priv->sso_verifier) {
+ fb_http_values_set_str(prms, "code_verifier", priv->sso_verifier);
+ g_free(priv->sso_verifier);
+ priv->sso_verifier = NULL;
+ }
+
+ if (priv->work_community_id) {
+ fb_http_values_set_int(prms, "community_id", priv->work_community_id);
+ }
+
+ if (priv->is_work && priv->token) {
+ fb_http_values_set_str(prms, "access_token", priv->token);
+ }
+
fb_api_http_req(api, FB_API_URL_AUTH, "authenticate", "auth.login", prms,
fb_api_cb_auth);
}
+static void
+fb_api_cb_work_prelogin(FbHttpRequest *req, gpointer data)
+{
+ FbApiPreloginData *pata = data;
+ FbApi *api = pata->api;
+ FbApiPrivate *priv = api->priv;
+ GError *err = NULL;
+ JsonNode *root;
+ gchar *status;
+ gchar *user = pata->user;
+ gchar *pass = pata->pass;
+
+ g_free(pata);
+
+ if (!fb_api_http_chk(api, req, &root)) {
+ return;
+ }
+
+ status = fb_json_node_get_str(root, "$.status", &err);
+
+ FB_API_ERROR_EMIT(api, err,
+ json_node_free(root);
+ return;
+ );
+
+ if (g_strcmp0(status, "can_login_password") == 0) {
+ fb_api_auth(api, user, pass, "work_account_password");
+
+ } else if (g_strcmp0(status, "can_login_via_linked_account") == 0) {
+ fb_api_auth(api, user, pass, "personal_account_password_with_work_username");
+ priv->need_work_switch = TRUE;
+
+ } else if (g_strcmp0(status, "can_login_sso") == 0) {
+ g_signal_emit_by_name(api, "work-sso-login");
+
+ } else if (g_strcmp0(status, "cannot_login") == 0) {
+ char *reason = fb_json_node_get_str(root, "$.cannot_login_reason", NULL);
+
+ if (g_strcmp0(reason, "non_business_email") == 0) {
+ fb_api_error(api, FB_API_ERROR_AUTH,
+ "Cannot login with non-business email. "
+ "Change the 'username' setting or disable 'work'");
+ } else {
+ char *title = fb_json_node_get_str(root, "$.error_title", NULL);
+ char *body = fb_json_node_get_str(root, "$.error_body", NULL);
+
+ fb_api_error(api, FB_API_ERROR_AUTH,
+ "Work prelogin failed (%s - %s)", title, body);
+
+ g_free(title);
+ g_free(body);
+ }
+
+ g_free(reason);
+
+ } else if (g_strcmp0(status, "can_self_invite") == 0) {
+ fb_api_error(api, FB_API_ERROR_AUTH, "Unknown email. "
+ "Change the 'username' setting or disable 'work'");
+ }
+
+ g_free(status);
+ json_node_free(root);
+}
+
+void
+fb_api_work_login(FbApi *api, gchar *user, gchar *pass)
+{
+ FbApiPrivate *priv = api->priv;
+ FbHttpRequest *req;
+ FbHttpValues *prms, *hdrs;
+ FbApiPreloginData *pata = g_new0(FbApiPreloginData, 1);
+
+ pata->api = api;
+ pata->user = user;
+ pata->pass = pass;
+
+ priv->is_work = TRUE;
+
+ req = fb_http_request_new(priv->http, FB_API_URL_WORK_PRELOGIN, TRUE,
+ fb_api_cb_work_prelogin, pata);
+
+ hdrs = fb_http_request_get_headers(req);
+ fb_http_values_set_str(hdrs, "Authorization", "OAuth null");
+
+ prms = fb_http_request_get_params(req);
+ fb_http_values_set_str(prms, "email", user);
+ fb_http_values_set_str(prms, "access_token",
+ FB_WORK_API_KEY "|" FB_WORK_API_SECRET);
+
+ fb_http_request_send(req);
+}
+
+gchar *
+fb_api_work_gen_sso_url(FbApi *api, const gchar *user)
+{
+ FbApiPrivate *priv = api->priv;
+ gchar *challenge, *verifier, *req_id, *email;
+ gchar *ret;
+
+ fb_util_gen_sso_verifier(&challenge, &verifier, &req_id);
+
+ email = g_uri_escape_string(user, NULL, FALSE);
+
+ ret = g_strdup_printf(FB_API_SSO_URL, req_id, challenge, email);
+
+ g_free(req_id);
+ g_free(challenge);
+ g_free(email);
+
+ g_free(priv->sso_verifier);
+ priv->sso_verifier = verifier;
+
+ return ret;
+}
+
+void
+fb_api_work_got_nonce(FbApi *api, const gchar *url)
+{
+ gchar **split;
+ gchar *uid = NULL;
+ gchar *nonce = NULL;
+ int i;
+
+ if (!g_str_has_prefix(url, "fb-workchat-sso://sso/?")) {
+ return;
+ }
+
+ split = g_strsplit(strchr(url, '?'), "&", -1);
+
+ for (i = 0; split[i]; i++) {
+ gchar *eq = strchr(split[i], '=');
+
+ if (g_str_has_prefix(split[i], "uid=")) {
+ uid = g_strstrip(eq + 1);
+ } else if (g_str_has_prefix(split[i], "nonce=")) {
+ nonce = g_strstrip(eq + 1);
+ }
+ }
+
+ if (uid && nonce) {
+ fb_api_auth(api, uid, nonce, "work_sso_nonce");
+ }
+
+ g_strfreev(split);
+}
+
static gchar *
fb_api_user_icon_checksum(gchar *icon)
{
@@ -2257,6 +2518,8 @@ fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users)
"$.represented_profile.id");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.represented_profile.friendship_status");
+ fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE,
+ "$.is_on_viewer_contact_list");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.structured_name.text");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
@@ -2269,11 +2532,14 @@ fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users)
}
while (fb_json_values_update(values, &err)) {
+ gboolean in_contact_list;
+
str = fb_json_values_next_str(values, "0");
uid = FB_ID_FROM_STR(str);
str = fb_json_values_next_str(values, NULL);
+ in_contact_list = fb_json_values_next_bool(values, FALSE);
- if (((g_strcmp0(str, "ARE_FRIENDS") != 0) &&
+ if ((!in_contact_list && (g_strcmp0(str, "ARE_FRIENDS") != 0) &&
(uid != priv->uid)) || (uid == 0))
{
if (!is_array) {