source: protocols/jabber/sasl.c @ 64b6635

Last change on this file since 64b6635 was 64b6635, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-12-18T21:56:44Z

Restructured and updated code a little bit to support new-style (much better
and "proper" OAuth2) Facebook OAuth support. (And, add wl.offline scope to get
tokens that don't expire after an hour.)

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