source: protocols/jabber/sasl.c @ a07a8c2

Last change on this file since a07a8c2 was 0872bb2, checked in by Wilmer van der Gaast <wilmer@…>, at 2012-03-10T00:00:25Z

Fix bug that broke MSN-XMPP with confusing error messages.

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