source: protocols/jabber/sasl.c @ df67b48

Last change on this file since df67b48 was 67ea361, checked in by dequis <dx@…>, at 2016-08-30T20:40:19Z

hipchat: Add basic support for personal oauth tokens

Fixes trac ticket 1265 - see the comments on that ticket for reasons on
why personal tokens and not the usual oauth flow. TL;DR hipchat doesn't
allow third party apps to own oauth client secrets. Instead, those are
generated when the "addon" is "installed" which requires a flow that is
either impossible or too awkward to use in bitlbee.

So, after giving up on the right way and telling the users to manage
tokens the ugly way, what's left to do is easy, just a few tweaks in the
sasl blob and short-circuit most of the actual oauth stuff. I didn't
even bother changing the service struct from google. It's not used.

This also updates the gtalk SASL X-OAUTH2 code to use GStrings instead of
juggling with malloc/strlen/strcpy, and simplifies a bit the way
GStrings are used in the equivalent SASL PLAIN code.

  • Property mode set to 100644
File size: 15.5 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};
40
41/* """"""""""""""""""""""""""""""oauth"""""""""""""""""""""""""""""" */
42#define HIPCHAT_SO_CALLED_OAUTH_URL "https://hipchat.com/account/api"
43
44xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
45{
46        struct im_connection *ic = data;
47        struct jabber_data *jd = ic->proto_data;
48        struct xt_node *c, *reply;
49        char *s;
50        int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_hipchat_oauth = 0;
51        int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE;
52        GString *mechs;
53
54        if (!sasl_supported(ic)) {
55                /* Should abort this now, since we should already be doing
56                   IQ authentication. Strange things happen when you try
57                   to do both... */
58                imcb_log(ic,
59                         "XMPP 1.0 non-compliant server seems to support SASL, please report this as a BitlBee bug!");
60                return XT_HANDLED;
61        }
62
63        s = xt_find_attr(node, "xmlns");
64        if (!s || strcmp(s, XMLNS_SASL) != 0) {
65                imcb_log(ic, "Stream error while authenticating");
66                imc_logout(ic, FALSE);
67                return XT_ABORT;
68        }
69
70        want_anonymous = set_getbool(&ic->acc->set, "anonymous");
71        want_oauth = set_getbool(&ic->acc->set, "oauth");
72        want_hipchat = (jd->flags & JFLAG_HIPCHAT);
73
74        mechs = g_string_new("");
75        c = node->children;
76        while ((c = xt_find_node(c, "mechanism"))) {
77                if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
78                        sup_plain = 1;
79                } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
80                        sup_digest = 1;
81                } else if (c->text && g_strcasecmp(c->text, "ANONYMOUS") == 0) {
82                        sup_anonymous = 1;
83                } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
84                        sup_gtalk = 1;
85                } else if (c->text && g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) {
86                        sup_hipchat_oauth = 1;
87                }
88
89                if (c->text) {
90                        g_string_append_printf(mechs, " %s", c->text);
91                }
92
93                c = c->next;
94        }
95
96        if (!want_oauth && !sup_plain && !sup_digest) {
97                if (sup_gtalk || sup_hipchat_oauth) {
98                        imcb_error(ic, "This server requires OAuth "
99                                   "(supported schemes:%s)", mechs->str);
100                } else {
101                        imcb_error(ic, "BitlBee does not support any of the offered SASL "
102                                   "authentication schemes:%s", mechs->str);
103                }
104                imc_logout(ic, FALSE);
105                g_string_free(mechs, TRUE);
106                return XT_ABORT;
107        }
108        g_string_free(mechs, TRUE);
109
110        reply = xt_new_node("auth", NULL, NULL);
111        if (!want_hipchat) {
112                xt_add_attr(reply, "xmlns", XMLNS_SASL);
113        } else {
114                xt_add_attr(reply, "xmlns", XMLNS_HIPCHAT);
115        }
116
117        if ((sup_gtalk || sup_hipchat_oauth) && want_oauth) {
118                GString *gs;
119
120                gs = g_string_sized_new(128);
121
122                g_string_append_c(gs, '\0');
123
124                if (sup_gtalk) {
125                        /* X-OAUTH2 is not *the* standard OAuth2 SASL/XMPP implementation.
126                           It's currently used by GTalk and vaguely documented on
127                           http://code.google.com/apis/cloudprint/docs/rawxmpp.html */
128                        xt_add_attr(reply, "mechanism", "X-OAUTH2");
129
130                        g_string_append(gs, jd->username);
131                        g_string_append_c(gs, '\0');
132                        g_string_append(gs, jd->oauth2_access_token);
133                } else if (sup_hipchat_oauth) {
134                        /* Hipchat's variant, not standard either, is documented here:
135                           https://docs.atlassian.com/hipchat.xmpp/latest/xmpp/auth.html */
136                        xt_add_attr(reply, "mechanism", "oauth2");
137
138                        g_string_append(gs, jd->oauth2_access_token);
139                        g_string_append_c(gs, '\0');
140                        g_string_append(gs, set_getstr(&ic->acc->set, "resource"));
141                }
142
143                reply->text = base64_encode((unsigned char *) gs->str, gs->len);
144                reply->text_len = strlen(reply->text);
145                g_string_free(gs, TRUE);
146
147        } else if (want_oauth) {
148                imcb_error(ic, "OAuth requested, but not supported by server");
149                imc_logout(ic, FALSE);
150                xt_free_node(reply);
151                return XT_ABORT;
152        } else if (want_anonymous && sup_anonymous) {
153                xt_add_attr(reply, "mechanism", "ANONYMOUS");
154
155                /* Well, that was easy. */
156        } else if (want_anonymous) {
157                imcb_error(ic, "Anonymous login requested, but not supported by server");
158                imc_logout(ic, FALSE);
159                xt_free_node(reply);
160                return XT_ABORT;
161        } else if (sup_digest && !(jd->ssl && sup_plain)) {
162                /* Only try DIGEST-MD5 if there's no SSL/TLS or if PLAIN isn't supported.
163                 * Which in practice means "don't bother with DIGEST-MD5 most of the time".
164                 * It's weak, pointless over TLS, and often breaks with some servers (hi openfire) */
165
166                xt_add_attr(reply, "mechanism", "DIGEST-MD5");
167
168                /* The rest will be done later, when we receive a <challenge/>. */
169        } else if (sup_plain) {
170                GString *gs;
171                char *username;
172
173                if (!want_hipchat) {
174                        xt_add_attr(reply, "mechanism", "PLAIN");
175                        username = jd->username;
176                } else {
177                        username = jd->me;
178                }
179
180                /* set an arbitrary initial size to avoid reallocations */
181                gs = g_string_sized_new(128);
182
183                /* With SASL PLAIN in XMPP, the text should be b64(\0user\0pass) */
184                g_string_append_c(gs, '\0');
185                g_string_append(gs, username);
186                g_string_append_c(gs, '\0');
187                g_string_append(gs, ic->acc->pass);
188                if (want_hipchat) {
189                        /* Hipchat's variation adds \0resource at the end */
190                        g_string_append_c(gs, '\0');
191                        g_string_append(gs, set_getstr(&ic->acc->set, "resource"));
192                }
193
194                reply->text = base64_encode((unsigned char *) gs->str, gs->len);
195                reply->text_len = strlen(reply->text);
196                g_string_free(gs, TRUE);
197        }
198
199        if (reply && !jabber_write_packet(ic, reply)) {
200                xt_free_node(reply);
201                return XT_ABORT;
202        }
203        xt_free_node(reply);
204
205        /* To prevent classic authentication from happening. */
206        jd->flags |= JFLAG_STREAM_STARTED;
207
208        return XT_HANDLED;
209}
210
211/* Non-static function, but not mentioned in jabber.h because it's for internal
212   use, just that the unittest should be able to reach it... */
213char *sasl_get_part(char *data, char *field)
214{
215        int i, len;
216
217        len = strlen(field);
218
219        while (g_ascii_isspace(*data) || *data == ',') {
220                data++;
221        }
222
223        if (g_strncasecmp(data, field, len) == 0 && data[len] == '=') {
224                i = strlen(field) + 1;
225        } else {
226                for (i = 0; data[i]; i++) {
227                        /* If we have a ", skip until it's closed again. */
228                        if (data[i] == '"') {
229                                i++;
230                                while (data[i] != '"' || data[i - 1] == '\\') {
231                                        i++;
232                                }
233                        }
234
235                        /* If we got a comma, we got a new field. Check it,
236                           find the next key after it. */
237                        if (data[i] == ',') {
238                                while (g_ascii_isspace(data[i]) || data[i] == ',') {
239                                        i++;
240                                }
241
242                                if (g_strncasecmp(data + i, field, len) == 0 &&
243                                    data[i + len] == '=') {
244                                        i += len + 1;
245                                        break;
246                                }
247                        }
248                }
249        }
250
251        if (data[i] == '"') {
252                int j;
253                char *ret;
254
255                i++;
256                len = 0;
257                while (data[i + len] != '"' || data[i + len - 1] == '\\') {
258                        len++;
259                }
260
261                ret = g_strndup(data + i, len);
262                for (i = j = 0; ret[i]; i++) {
263                        if (ret[i] == '\\') {
264                                ret[j++] = ret[++i];
265                        } else {
266                                ret[j++] = ret[i];
267                        }
268                }
269                ret[j] = 0;
270
271                return ret;
272        } else if (data[i]) {
273                len = 0;
274                while (data[i + len] && data[i + len] != ',') {
275                        len++;
276                }
277
278                return g_strndup(data + i, len);
279        } else {
280                return NULL;
281        }
282}
283
284xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
285{
286        struct im_connection *ic = data;
287        struct jabber_data *jd = ic->proto_data;
288        struct xt_node *reply_pkt = NULL;
289        char *nonce = NULL, *realm = NULL, *cnonce = NULL;
290        unsigned char cnonce_bin[30];
291        char *digest_uri = NULL;
292        char *dec = NULL;
293        char *s = NULL, *reply = NULL;
294        xt_status ret = XT_ABORT;
295
296        if (node->text_len == 0) {
297                goto error;
298        }
299
300        dec = frombase64(node->text);
301
302        if (!(s = sasl_get_part(dec, "rspauth"))) {
303                /* See RFC 2831 for for information. */
304                md5_state_t A1, A2, H;
305                md5_byte_t A1r[16], A2r[16], Hr[16];
306                char A1h[33], A2h[33], Hh[33];
307                int i;
308
309                nonce = sasl_get_part(dec, "nonce");
310                realm = sasl_get_part(dec, "realm");
311
312                if (!nonce) {
313                        goto error;
314                }
315
316                /* Jabber.Org considers the realm part optional and doesn't
317                   specify one. Oh well, actually they're right, but still,
318                   don't know if this is right... */
319                if (!realm) {
320                        realm = g_strdup(jd->server);
321                }
322
323                random_bytes(cnonce_bin, sizeof(cnonce_bin));
324                cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
325                digest_uri = g_strdup_printf("%s/%s", "xmpp", jd->server);
326
327                /* Generate the MD5 hash of username:realm:password,
328                   I decided to call it H. */
329                md5_init(&H);
330                s = g_strdup_printf("%s:%s:%s", jd->username, realm, ic->acc->pass);
331                md5_append(&H, (unsigned char *) s, strlen(s));
332                g_free(s);
333                md5_finish(&H, Hr);
334
335                /* Now generate the hex. MD5 hash of H:nonce:cnonce, called A1. */
336                md5_init(&A1);
337                s = g_strdup_printf(":%s:%s", nonce, cnonce);
338                md5_append(&A1, Hr, 16);
339                md5_append(&A1, (unsigned char *) s, strlen(s));
340                g_free(s);
341                md5_finish(&A1, A1r);
342                for (i = 0; i < 16; i++) {
343                        sprintf(A1h + i * 2, "%02x", A1r[i]);
344                }
345
346                /* A2... */
347                md5_init(&A2);
348                s = g_strdup_printf("%s:%s", "AUTHENTICATE", digest_uri);
349                md5_append(&A2, (unsigned char *) s, strlen(s));
350                g_free(s);
351                md5_finish(&A2, A2r);
352                for (i = 0; i < 16; i++) {
353                        sprintf(A2h + i * 2, "%02x", A2r[i]);
354                }
355
356                /* Final result: A1:nonce:00000001:cnonce:auth:A2. Let's reuse H for it. */
357                md5_init(&H);
358                s = g_strdup_printf("%s:%s:%s:%s:%s:%s", A1h, nonce, "00000001", cnonce, "auth", A2h);
359                md5_append(&H, (unsigned char *) s, strlen(s));
360                g_free(s);
361                md5_finish(&H, Hr);
362                for (i = 0; i < 16; i++) {
363                        sprintf(Hh + i * 2, "%02x", Hr[i]);
364                }
365
366                /* Now build the SASL response string: */
367                reply = g_strdup_printf("username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\","
368                                        "nc=%08x,qop=auth,digest-uri=\"%s\",response=%s,charset=%s",
369                                        jd->username, realm, nonce, cnonce, 1, digest_uri, Hh, "utf-8");
370        } else {
371                /* We found rspauth, but don't really care... */
372                g_free(s);
373        }
374
375        s = reply ? tobase64(reply) : NULL;
376        reply_pkt = xt_new_node("response", s, NULL);
377        xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL);
378
379        if (!jabber_write_packet(ic, reply_pkt)) {
380                goto silent_error;
381        }
382
383        ret = XT_HANDLED;
384        goto silent_error;
385
386error:
387        imcb_error(ic, "Incorrect SASL challenge received");
388        imc_logout(ic, FALSE);
389
390silent_error:
391        g_free(digest_uri);
392        g_free(cnonce);
393        g_free(nonce);
394        g_free(reply);
395        g_free(realm);
396        g_free(dec);
397        g_free(s);
398        xt_free_node(reply_pkt);
399
400        return ret;
401}
402
403xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
404{
405        struct im_connection *ic = data;
406        struct jabber_data *jd = ic->proto_data;
407        char *s;
408
409        s = xt_find_attr(node, "xmlns");
410        if (!s || strcmp(s, XMLNS_SASL) != 0) {
411                imcb_log(ic, "Stream error while authenticating");
412                imc_logout(ic, FALSE);
413                return XT_ABORT;
414        }
415
416        if (strcmp(node->name, "success") == 0) {
417                imcb_log(ic, "Authentication finished");
418                jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
419
420                if (jd->flags & JFLAG_HIPCHAT) {
421                        return hipchat_handle_success(ic, node);
422                }
423        } else if (strcmp(node->name, "failure") == 0) {
424                imcb_error(ic, "Authentication failure");
425                imc_logout(ic, FALSE);
426                return XT_ABORT;
427        }
428
429        return XT_HANDLED;
430}
431
432/* This one is needed to judge if we'll do authentication using IQ or SASL.
433   It's done by checking if the <stream:stream> from the server has a
434   version attribute. I don't know if this is the right way though... */
435gboolean sasl_supported(struct im_connection *ic)
436{
437        struct jabber_data *jd = ic->proto_data;
438
439        return (jd->xt && jd->xt->root && xt_find_attr(jd->xt->root, "version")) != 0;
440}
441
442void sasl_oauth2_init(struct im_connection *ic)
443{
444        struct jabber_data *jd = ic->proto_data;
445
446        imcb_log(ic, "Starting OAuth authentication");
447
448        /* Temporary contact, just used to receive the OAuth response. */
449        imcb_add_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
450
451        if (jd->flags & JFLAG_HIPCHAT) {
452                imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE,
453                        "Open this URL and generate a token with 'View Group' and 'Send Message' scopes: "
454                        HIPCHAT_SO_CALLED_OAUTH_URL, 0, 0);
455        } else {
456                char *msg, *url;
457
458                url = oauth2_url(jd->oauth2_service);
459                msg = g_strdup_printf("Open this URL in your browser to authenticate: %s", url);
460                imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, msg, 0, 0);
461
462                g_free(msg);
463                g_free(url);
464        }
465        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, "Respond to this message with the returned "
466                       "authorization token.", 0, 0);
467
468}
469
470static gboolean sasl_oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond)
471{
472        struct im_connection *ic = data;
473
474        if (g_slist_find(jabber_connections, ic)) {
475                imcb_remove_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
476        }
477        return FALSE;
478}
479
480int sasl_oauth2_get_refresh_token(struct im_connection *ic, const char *msg)
481{
482        struct jabber_data *jd = ic->proto_data;
483        char *code;
484        int ret;
485
486        imcb_log(ic, "Requesting OAuth access token");
487
488        /* Don't do it here because the caller may get confused if the contact
489           we're currently sending a message to is deleted. */
490        b_timeout_add(1, sasl_oauth2_remove_contact, ic);
491
492        code = g_strdup(msg);
493        g_strstrip(code);
494        ret = oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_CODE,
495                                  code, sasl_oauth2_got_token, ic);
496
497        g_free(code);
498        return ret;
499}
500
501int sasl_oauth2_refresh(struct im_connection *ic, const char *refresh_token)
502{
503        struct jabber_data *jd = ic->proto_data;
504
505        return oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_REFRESH,
506                                   refresh_token, sasl_oauth2_got_token, ic);
507}
508
509void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error)
510{
511        struct im_connection *ic = data;
512        struct jabber_data *jd;
513        GSList *auth = NULL;
514
515        if (g_slist_find(jabber_connections, ic) == NULL) {
516                return;
517        }
518
519        jd = ic->proto_data;
520
521        if (access_token == NULL) {
522                imcb_error(ic, "OAuth failure (%s)", error);
523                imc_logout(ic, TRUE);
524                return;
525        }
526
527        oauth_params_parse(&auth, ic->acc->pass);
528        if (refresh_token) {
529                oauth_params_set(&auth, "refresh_token", refresh_token);
530        }
531        if (access_token) {
532                oauth_params_set(&auth, "access_token", access_token);
533        }
534
535        g_free(ic->acc->pass);
536        ic->acc->pass = oauth_params_string(auth);
537        oauth_params_free(&auth);
538
539        g_free(jd->oauth2_access_token);
540        jd->oauth2_access_token = g_strdup(access_token);
541
542        jabber_connect(ic);
543}
Note: See TracBrowser for help on using the repository browser.