source: protocols/jabber/sasl.c @ d93c8beb

Last change on this file since d93c8beb was 8eb2e84, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-02-24T23:23:07Z

Use https:// URL for Facebook OAuth2 now that there's a cert.

  • Property mode set to 100644
File size: 14.8 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Jabber module - SASL authentication                                      *
5*                                                                           *
6*  Copyright 2006-2012 Wilmer van der Gaast <wilmer@gaast.net>              *
7*                                                                           *
8*  This program is free software; you can redistribute it and/or modify     *
9*  it under the terms of the GNU General Public License as published by     *
10*  the Free Software Foundation; either version 2 of the License, or        *
11*  (at your option) any later version.                                      *
12*                                                                           *
13*  This program is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
16*  GNU General Public License for more details.                             *
17*                                                                           *
18*  You should have received a copy of the GNU General Public License along  *
19*  with this program; if not, write to the Free Software Foundation, Inc.,  *
20*  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              *
21*                                                                           *
22\***************************************************************************/
23
24#include <ctype.h>
25
26#include "jabber.h"
27#include "base64.h"
28#include "oauth2.h"
29#include "oauth.h"
30
31const struct oauth2_service oauth2_service_google =
32{
33        "https://accounts.google.com/o/oauth2/auth",
34        "https://accounts.google.com/o/oauth2/token",
35        "urn:ietf:wg:oauth:2.0:oob",
36        "https://www.googleapis.com/auth/googletalk",
37        "783993391592.apps.googleusercontent.com",
38        "6C-Zgf7Tr7gEQTPlBhMUgo7R",
39};
40const struct oauth2_service oauth2_service_facebook =
41{
42        "https://www.facebook.com/dialog/oauth",
43        "https://graph.facebook.com/oauth/access_token",
44        "https://www.bitlbee.org/main.php/Facebook/oauth2.html",
45        "offline_access,xmpp_login",
46        "126828914005625",
47        "4b100f0f244d620bf3f15f8b217d4c32",
48};
49
50xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
51{
52        struct im_connection *ic = data;
53        struct jabber_data *jd = ic->proto_data;
54        struct xt_node *c, *reply;
55        char *s;
56        int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_fb = 0;
57        int want_oauth = FALSE;
58        GString *mechs;
59
60        if (!sasl_supported(ic)) {
61                /* Should abort this now, since we should already be doing
62                   IQ authentication. Strange things happen when you try
63                   to do both... */
64                imcb_log(ic,
65                         "XMPP 1.0 non-compliant server seems to support SASL, please report this as a BitlBee bug!");
66                return XT_HANDLED;
67        }
68
69        s = xt_find_attr(node, "xmlns");
70        if (!s || strcmp(s, XMLNS_SASL) != 0) {
71                imcb_log(ic, "Stream error while authenticating");
72                imc_logout(ic, FALSE);
73                return XT_ABORT;
74        }
75
76        want_oauth = set_getbool(&ic->acc->set, "oauth");
77
78        mechs = g_string_new("");
79        c = node->children;
80        while ((c = xt_find_node(c, "mechanism"))) {
81                if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
82                        sup_plain = 1;
83                } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
84                        sup_digest = 1;
85                } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
86                        sup_gtalk = 1;
87                } else if (c->text && g_strcasecmp(c->text, "X-FACEBOOK-PLATFORM") == 0) {
88                        sup_fb = 1;
89                }
90
91                if (c->text) {
92                        g_string_append_printf(mechs, " %s", c->text);
93                }
94
95                c = c->next;
96        }
97
98        if (!want_oauth && !sup_plain && !sup_digest) {
99                if (!sup_gtalk && !sup_fb) {
100                        imcb_error(ic, "This server requires OAuth "
101                                   "(supported schemes:%s)", mechs->str);
102                } else {
103                        imcb_error(ic, "BitlBee does not support any of the offered SASL "
104                                   "authentication schemes:%s", mechs->str);
105                }
106                imc_logout(ic, FALSE);
107                g_string_free(mechs, TRUE);
108                return XT_ABORT;
109        }
110        g_string_free(mechs, TRUE);
111
112        reply = xt_new_node("auth", NULL, NULL);
113        xt_add_attr(reply, "xmlns", XMLNS_SASL);
114
115        if (sup_gtalk && want_oauth) {
116                int len;
117
118                /* X-OAUTH2 is, not *the* standard OAuth2 SASL/XMPP implementation.
119                   It's currently used by GTalk and vaguely documented on
120                   http://code.google.com/apis/cloudprint/docs/rawxmpp.html . */
121                xt_add_attr(reply, "mechanism", "X-OAUTH2");
122
123                len = strlen(jd->username) + strlen(jd->oauth2_access_token) + 2;
124                s = g_malloc(len + 1);
125                s[0] = 0;
126                strcpy(s + 1, jd->username);
127                strcpy(s + 2 + strlen(jd->username), jd->oauth2_access_token);
128                reply->text = base64_encode((unsigned char *) s, len);
129                reply->text_len = strlen(reply->text);
130                g_free(s);
131        } else if (sup_fb && want_oauth) {
132                xt_add_attr(reply, "mechanism", "X-FACEBOOK-PLATFORM");
133                jd->flags |= JFLAG_SASL_FB;
134        } else if (want_oauth) {
135                imcb_error(ic, "OAuth requested, but not supported by server");
136                imc_logout(ic, FALSE);
137                xt_free_node(reply);
138                return XT_ABORT;
139        } else if (sup_digest) {
140                xt_add_attr(reply, "mechanism", "DIGEST-MD5");
141
142                /* The rest will be done later, when we receive a <challenge/>. */
143        } else if (sup_plain) {
144                int len;
145
146                xt_add_attr(reply, "mechanism", "PLAIN");
147
148                /* With SASL PLAIN in XMPP, the text should be b64(\0user\0pass) */
149                len = strlen(jd->username) + strlen(ic->acc->pass) + 2;
150                s = g_malloc(len + 1);
151                s[0] = 0;
152                strcpy(s + 1, jd->username);
153                strcpy(s + 2 + strlen(jd->username), ic->acc->pass);
154                reply->text = base64_encode((unsigned char *) s, len);
155                reply->text_len = strlen(reply->text);
156                g_free(s);
157        }
158
159        if (reply && !jabber_write_packet(ic, reply)) {
160                xt_free_node(reply);
161                return XT_ABORT;
162        }
163        xt_free_node(reply);
164
165        /* To prevent classic authentication from happening. */
166        jd->flags |= JFLAG_STREAM_STARTED;
167
168        return XT_HANDLED;
169}
170
171/* Non-static function, but not mentioned in jabber.h because it's for internal
172   use, just that the unittest should be able to reach it... */
173char *sasl_get_part(char *data, char *field)
174{
175        int i, len;
176
177        len = strlen(field);
178
179        while (g_ascii_isspace(*data) || *data == ',') {
180                data++;
181        }
182
183        if (g_strncasecmp(data, field, len) == 0 && data[len] == '=') {
184                i = strlen(field) + 1;
185        } else {
186                for (i = 0; data[i]; i++) {
187                        /* If we have a ", skip until it's closed again. */
188                        if (data[i] == '"') {
189                                i++;
190                                while (data[i] != '"' || data[i - 1] == '\\') {
191                                        i++;
192                                }
193                        }
194
195                        /* If we got a comma, we got a new field. Check it,
196                           find the next key after it. */
197                        if (data[i] == ',') {
198                                while (g_ascii_isspace(data[i]) || data[i] == ',') {
199                                        i++;
200                                }
201
202                                if (g_strncasecmp(data + i, field, len) == 0 &&
203                                    data[i + len] == '=') {
204                                        i += len + 1;
205                                        break;
206                                }
207                        }
208                }
209        }
210
211        if (data[i] == '"') {
212                int j;
213                char *ret;
214
215                i++;
216                len = 0;
217                while (data[i + len] != '"' || data[i + len - 1] == '\\') {
218                        len++;
219                }
220
221                ret = g_strndup(data + i, len);
222                for (i = j = 0; ret[i]; i++) {
223                        if (ret[i] == '\\') {
224                                ret[j++] = ret[++i];
225                        } else {
226                                ret[j++] = ret[i];
227                        }
228                }
229                ret[j] = 0;
230
231                return ret;
232        } else if (data[i]) {
233                len = 0;
234                while (data[i + len] && data[i + len] != ',') {
235                        len++;
236                }
237
238                return g_strndup(data + i, len);
239        } else {
240                return NULL;
241        }
242}
243
244xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
245{
246        struct im_connection *ic = data;
247        struct jabber_data *jd = ic->proto_data;
248        struct xt_node *reply_pkt = NULL;
249        char *nonce = NULL, *realm = NULL, *cnonce = NULL;
250        unsigned char cnonce_bin[30];
251        char *digest_uri = NULL;
252        char *dec = NULL;
253        char *s = NULL, *reply = NULL;
254        xt_status ret = XT_ABORT;
255
256        if (node->text_len == 0) {
257                goto error;
258        }
259
260        dec = frombase64(node->text);
261
262        if (jd->flags & JFLAG_SASL_FB) {
263                /* New-style Facebook OAauth2 support. Instead of sending a refresh
264                   token, they just send an access token that should never expire. */
265                GSList *p_in = NULL, *p_out = NULL;
266                char time[33];
267
268                oauth_params_parse(&p_in, dec);
269                oauth_params_add(&p_out, "nonce", oauth_params_get(&p_in, "nonce"));
270                oauth_params_add(&p_out, "method", oauth_params_get(&p_in, "method"));
271                oauth_params_free(&p_in);
272
273                g_snprintf(time, sizeof(time), "%lld", (long long) (gettime() * 1000));
274                oauth_params_add(&p_out, "call_id", time);
275                oauth_params_add(&p_out, "api_key", oauth2_service_facebook.consumer_key);
276                oauth_params_add(&p_out, "v", "1.0");
277                oauth_params_add(&p_out, "format", "XML");
278                oauth_params_add(&p_out, "access_token", jd->oauth2_access_token);
279
280                reply = oauth_params_string(p_out);
281                oauth_params_free(&p_out);
282        } else if (!(s = sasl_get_part(dec, "rspauth"))) {
283                /* See RFC 2831 for for information. */
284                md5_state_t A1, A2, H;
285                md5_byte_t A1r[16], A2r[16], Hr[16];
286                char A1h[33], A2h[33], Hh[33];
287                int i;
288
289                nonce = sasl_get_part(dec, "nonce");
290                realm = sasl_get_part(dec, "realm");
291
292                if (!nonce) {
293                        goto error;
294                }
295
296                /* Jabber.Org considers the realm part optional and doesn't
297                   specify one. Oh well, actually they're right, but still,
298                   don't know if this is right... */
299                if (!realm) {
300                        realm = g_strdup(jd->server);
301                }
302
303                random_bytes(cnonce_bin, sizeof(cnonce_bin));
304                cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
305                digest_uri = g_strdup_printf("%s/%s", "xmpp", jd->server);
306
307                /* Generate the MD5 hash of username:realm:password,
308                   I decided to call it H. */
309                md5_init(&H);
310                s = g_strdup_printf("%s:%s:%s", jd->username, realm, ic->acc->pass);
311                md5_append(&H, (unsigned char *) s, strlen(s));
312                g_free(s);
313                md5_finish(&H, Hr);
314
315                /* Now generate the hex. MD5 hash of H:nonce:cnonce, called A1. */
316                md5_init(&A1);
317                s = g_strdup_printf(":%s:%s", nonce, cnonce);
318                md5_append(&A1, Hr, 16);
319                md5_append(&A1, (unsigned char *) s, strlen(s));
320                g_free(s);
321                md5_finish(&A1, A1r);
322                for (i = 0; i < 16; i++) {
323                        sprintf(A1h + i * 2, "%02x", A1r[i]);
324                }
325
326                /* A2... */
327                md5_init(&A2);
328                s = g_strdup_printf("%s:%s", "AUTHENTICATE", digest_uri);
329                md5_append(&A2, (unsigned char *) s, strlen(s));
330                g_free(s);
331                md5_finish(&A2, A2r);
332                for (i = 0; i < 16; i++) {
333                        sprintf(A2h + i * 2, "%02x", A2r[i]);
334                }
335
336                /* Final result: A1:nonce:00000001:cnonce:auth:A2. Let's reuse H for it. */
337                md5_init(&H);
338                s = g_strdup_printf("%s:%s:%s:%s:%s:%s", A1h, nonce, "00000001", cnonce, "auth", A2h);
339                md5_append(&H, (unsigned char *) s, strlen(s));
340                g_free(s);
341                md5_finish(&H, Hr);
342                for (i = 0; i < 16; i++) {
343                        sprintf(Hh + i * 2, "%02x", Hr[i]);
344                }
345
346                /* Now build the SASL response string: */
347                reply = g_strdup_printf("username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\","
348                                        "nc=%08x,qop=auth,digest-uri=\"%s\",response=%s,charset=%s",
349                                        jd->username, realm, nonce, cnonce, 1, digest_uri, Hh, "utf-8");
350        } else {
351                /* We found rspauth, but don't really care... */
352                g_free(s);
353        }
354
355        s = reply ? tobase64(reply) : NULL;
356        reply_pkt = xt_new_node("response", s, NULL);
357        xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL);
358
359        if (!jabber_write_packet(ic, reply_pkt)) {
360                goto silent_error;
361        }
362
363        ret = XT_HANDLED;
364        goto silent_error;
365
366error:
367        imcb_error(ic, "Incorrect SASL challenge received");
368        imc_logout(ic, FALSE);
369
370silent_error:
371        g_free(digest_uri);
372        g_free(cnonce);
373        g_free(nonce);
374        g_free(reply);
375        g_free(realm);
376        g_free(dec);
377        g_free(s);
378        xt_free_node(reply_pkt);
379
380        return ret;
381}
382
383xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
384{
385        struct im_connection *ic = data;
386        struct jabber_data *jd = ic->proto_data;
387        char *s;
388
389        s = xt_find_attr(node, "xmlns");
390        if (!s || strcmp(s, XMLNS_SASL) != 0) {
391                imcb_log(ic, "Stream error while authenticating");
392                imc_logout(ic, FALSE);
393                return XT_ABORT;
394        }
395
396        if (strcmp(node->name, "success") == 0) {
397                imcb_log(ic, "Authentication finished");
398                jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
399        } else if (strcmp(node->name, "failure") == 0) {
400                imcb_error(ic, "Authentication failure");
401                imc_logout(ic, FALSE);
402                return XT_ABORT;
403        }
404
405        return XT_HANDLED;
406}
407
408/* This one is needed to judge if we'll do authentication using IQ or SASL.
409   It's done by checking if the <stream:stream> from the server has a
410   version attribute. I don't know if this is the right way though... */
411gboolean sasl_supported(struct im_connection *ic)
412{
413        struct jabber_data *jd = ic->proto_data;
414
415        return (jd->xt && jd->xt->root && xt_find_attr(jd->xt->root, "version")) != 0;
416}
417
418void sasl_oauth2_init(struct im_connection *ic)
419{
420        struct jabber_data *jd = ic->proto_data;
421        char *msg, *url;
422
423        imcb_log(ic, "Starting OAuth authentication");
424
425        /* Temporary contact, just used to receive the OAuth response. */
426        imcb_add_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
427        url = oauth2_url(jd->oauth2_service);
428        msg = g_strdup_printf("Open this URL in your browser to authenticate: %s", url);
429        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, msg, 0, 0);
430        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, "Respond to this message with the returned "
431                       "authorization token.", 0, 0);
432
433        g_free(msg);
434        g_free(url);
435}
436
437static gboolean sasl_oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond)
438{
439        struct im_connection *ic = data;
440
441        if (g_slist_find(jabber_connections, ic)) {
442                imcb_remove_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
443        }
444        return FALSE;
445}
446
447static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token,
448                                  const char *error);
449
450int sasl_oauth2_get_refresh_token(struct im_connection *ic, const char *msg)
451{
452        struct jabber_data *jd = ic->proto_data;
453        char *code;
454        int ret;
455
456        imcb_log(ic, "Requesting OAuth access token");
457
458        /* Don't do it here because the caller may get confused if the contact
459           we're currently sending a message to is deleted. */
460        b_timeout_add(1, sasl_oauth2_remove_contact, ic);
461
462        code = g_strdup(msg);
463        g_strstrip(code);
464        ret = oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_CODE,
465                                  code, sasl_oauth2_got_token, ic);
466
467        g_free(code);
468        return ret;
469}
470
471int sasl_oauth2_refresh(struct im_connection *ic, const char *refresh_token)
472{
473        struct jabber_data *jd = ic->proto_data;
474
475        return oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_REFRESH,
476                                   refresh_token, sasl_oauth2_got_token, ic);
477}
478
479static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error)
480{
481        struct im_connection *ic = data;
482        struct jabber_data *jd;
483        GSList *auth = NULL;
484
485        if (g_slist_find(jabber_connections, ic) == NULL) {
486                return;
487        }
488
489        jd = ic->proto_data;
490
491        if (access_token == NULL) {
492                imcb_error(ic, "OAuth failure (%s)", error);
493                imc_logout(ic, TRUE);
494                return;
495        }
496
497        oauth_params_parse(&auth, ic->acc->pass);
498        if (refresh_token) {
499                oauth_params_set(&auth, "refresh_token", refresh_token);
500        }
501        if (access_token) {
502                oauth_params_set(&auth, "access_token", access_token);
503        }
504
505        g_free(ic->acc->pass);
506        ic->acc->pass = oauth_params_string(auth);
507        oauth_params_free(&auth);
508
509        g_free(jd->oauth2_access_token);
510        jd->oauth2_access_token = g_strdup(access_token);
511
512        jabber_connect(ic);
513}
Note: See TracBrowser for help on using the repository browser.