source: protocols/jabber/sasl.c @ 18c6d36

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

More generic OAuth support now. Should work well for all GTalk accounts now
and somewhat for MS Messenger. The fb part needs different parsing of the
authorize request, and possibly some other work.

  • Property mode set to 100644
File size: 15.8 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.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                if( c->text && g_strcasecmp( c->text, "DIGEST-MD5" ) == 0 )
91                        sup_digest = 1;
92                if( c->text && g_strcasecmp( c->text, "X-OAUTH2" ) == 0 )
93                        sup_gtalk = 1;
94                if( c->text && g_strcasecmp( c->text, "X-FACEBOOK-PLATFORM" ) == 0 )
95                        sup_fb = 1;
96                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 && strstr( ic->acc->pass, "session_key=" ) )
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, *p;
295                md5_state_t md5;
296                char time[33], *token;
297                const char *secret;
298               
299                oauth_params_parse( &p_in, dec );
300                oauth_params_add( &p_out, "nonce", oauth_params_get( &p_in, "nonce" ) );
301                oauth_params_add( &p_out, "method", oauth_params_get( &p_in, "method" ) );
302                oauth_params_free( &p_in );
303               
304                token = g_strdup( ic->acc->pass );
305                oauth_params_parse( &p_in, token );
306                g_free( token );
307                oauth_params_add( &p_out, "session_key", oauth_params_get( &p_in, "session_key" ) );
308               
309                g_snprintf( time, sizeof( time ), "%lld", (long long) ( gettime() * 1000 ) );
310                oauth_params_add( &p_out, "call_id", time );
311                oauth_params_add( &p_out, "api_key", oauth2_service_facebook.consumer_key );
312                oauth_params_add( &p_out, "v", "1.0" );
313                oauth_params_add( &p_out, "format", "XML" );
314               
315                md5_init( &md5 );
316                for( p = p_out; p; p = p->next )
317                        md5_append( &md5, p->data, strlen( p->data ) );
318               
319                secret = oauth_params_get( &p_in, "secret" );
320                if( secret )
321                        md5_append( &md5, (unsigned char*) secret, strlen( secret ) );
322                md5_finish_ascii( &md5, time );
323                oauth_params_add( &p_out, "sig", time );
324               
325                reply = oauth_params_string( p_out );
326                oauth_params_free( &p_out );
327                oauth_params_free( &p_in );
328        }
329        else if( !( s = sasl_get_part( dec, "rspauth" ) ) )
330        {
331                /* See RFC 2831 for for information. */
332                md5_state_t A1, A2, H;
333                md5_byte_t A1r[16], A2r[16], Hr[16];
334                char A1h[33], A2h[33], Hh[33];
335                int i;
336               
337                nonce = sasl_get_part( dec, "nonce" );
338                realm = sasl_get_part( dec, "realm" );
339               
340                if( !nonce )
341                        goto error;
342               
343                /* Jabber.Org considers the realm part optional and doesn't
344                   specify one. Oh well, actually they're right, but still,
345                   don't know if this is right... */
346                if( !realm )
347                        realm = g_strdup( jd->server );
348               
349                random_bytes( cnonce_bin, sizeof( cnonce_bin ) );
350                cnonce = base64_encode( cnonce_bin, sizeof( cnonce_bin ) );
351                digest_uri = g_strdup_printf( "%s/%s", "xmpp", jd->server );
352               
353                /* Generate the MD5 hash of username:realm:password,
354                   I decided to call it H. */
355                md5_init( &H );
356                s = g_strdup_printf( "%s:%s:%s", jd->username, realm, ic->acc->pass );
357                md5_append( &H, (unsigned char *) s, strlen( s ) );
358                g_free( s );
359                md5_finish( &H, Hr );
360               
361                /* Now generate the hex. MD5 hash of H:nonce:cnonce, called A1. */
362                md5_init( &A1 );
363                s = g_strdup_printf( ":%s:%s", nonce, cnonce );
364                md5_append( &A1, Hr, 16 );
365                md5_append( &A1, (unsigned char *) s, strlen( s ) );
366                g_free( s );
367                md5_finish( &A1, A1r );
368                for( i = 0; i < 16; i ++ )
369                        sprintf( A1h + i * 2, "%02x", A1r[i] );
370               
371                /* A2... */
372                md5_init( &A2 );
373                s = g_strdup_printf( "%s:%s", "AUTHENTICATE", digest_uri );
374                md5_append( &A2, (unsigned char *) s, strlen( s ) );
375                g_free( s );
376                md5_finish( &A2, A2r );
377                for( i = 0; i < 16; i ++ )
378                        sprintf( A2h + i * 2, "%02x", A2r[i] );
379               
380                /* Final result: A1:nonce:00000001:cnonce:auth:A2. Let's reuse H for it. */
381                md5_init( &H );
382                s = g_strdup_printf( "%s:%s:%s:%s:%s:%s", A1h, nonce, "00000001", cnonce, "auth", A2h );
383                md5_append( &H, (unsigned char *) s, strlen( s ) );
384                g_free( s );
385                md5_finish( &H, Hr );
386                for( i = 0; i < 16; i ++ )
387                        sprintf( Hh + i * 2, "%02x", Hr[i] );
388               
389                /* Now build the SASL response string: */
390                reply = g_strdup_printf( "username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\","
391                                         "nc=%08x,qop=auth,digest-uri=\"%s\",response=%s,charset=%s",
392                                         jd->username, realm, nonce, cnonce, 1, digest_uri, Hh, "utf-8" );
393        }
394        else
395        {
396                /* We found rspauth, but don't really care... */
397        }
398       
399        s = reply ? tobase64( reply ) : NULL;
400        reply_pkt = xt_new_node( "response", s, NULL );
401        xt_add_attr( reply_pkt, "xmlns", XMLNS_SASL );
402       
403        if( !jabber_write_packet( ic, reply_pkt ) )
404                goto silent_error;
405       
406        ret = XT_HANDLED;
407        goto silent_error;
408
409error:
410        imcb_error( ic, "Incorrect SASL challenge received" );
411        imc_logout( ic, FALSE );
412
413silent_error:
414        g_free( digest_uri );
415        g_free( cnonce );
416        g_free( nonce );
417        g_free( reply );
418        g_free( realm );
419        g_free( dec );
420        g_free( s );
421        xt_free_node( reply_pkt );
422       
423        return ret;
424}
425
426xt_status sasl_pkt_result( struct xt_node *node, gpointer data )
427{
428        struct im_connection *ic = data;
429        struct jabber_data *jd = ic->proto_data;
430        char *s;
431       
432        s = xt_find_attr( node, "xmlns" );
433        if( !s || strcmp( s, XMLNS_SASL ) != 0 )
434        {
435                imcb_log( ic, "Stream error while authenticating" );
436                imc_logout( ic, FALSE );
437                return XT_ABORT;
438        }
439       
440        if( strcmp( node->name, "success" ) == 0 )
441        {
442                imcb_log( ic, "Authentication finished" );
443                jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
444        }
445        else if( strcmp( node->name, "failure" ) == 0 )
446        {
447                imcb_error( ic, "Authentication failure" );
448                imc_logout( ic, FALSE );
449                return XT_ABORT;
450        }
451       
452        return XT_HANDLED;
453}
454
455/* This one is needed to judge if we'll do authentication using IQ or SASL.
456   It's done by checking if the <stream:stream> from the server has a
457   version attribute. I don't know if this is the right way though... */
458gboolean sasl_supported( struct im_connection *ic )
459{
460        struct jabber_data *jd = ic->proto_data;
461       
462        return ( jd->xt && jd->xt->root && xt_find_attr( jd->xt->root, "version" ) ) != 0;
463}
464
465void sasl_oauth2_init( struct im_connection *ic )
466{
467        struct jabber_data *jd = ic->proto_data;
468        char *msg, *url;
469       
470        imcb_log( ic, "Starting OAuth authentication" );
471       
472        /* Temporary contact, just used to receive the OAuth response. */
473        imcb_add_buddy( ic, "jabber_oauth", NULL );
474        url = oauth2_url( jd->oauth2_service );
475        msg = g_strdup_printf( "Open this URL in your browser to authenticate: %s", url );
476        imcb_buddy_msg( ic, "jabber_oauth", msg, 0, 0 );
477        imcb_buddy_msg( ic, "jabber_oauth", "Respond to this message with the returned "
478                                            "authorization token.", 0, 0 );
479       
480        g_free( msg );
481        g_free( url );
482}
483
484static gboolean sasl_oauth2_remove_contact( gpointer data, gint fd, b_input_condition cond )
485{
486        struct im_connection *ic = data;
487        if( g_slist_find( jabber_connections, ic ) )
488                imcb_remove_buddy( ic, "jabber_oauth", NULL );
489        return FALSE;
490}
491
492static void sasl_oauth2_got_token( gpointer data, const char *access_token, const char *refresh_token );
493
494int sasl_oauth2_get_refresh_token( struct im_connection *ic, const char *msg )
495{
496        struct jabber_data *jd = ic->proto_data;
497        char *code;
498        int ret;
499       
500        imcb_log( ic, "Requesting OAuth access token" );
501       
502        /* Don't do it here because the caller may get confused if the contact
503           we're currently sending a message to is deleted. */
504        b_timeout_add( 1, sasl_oauth2_remove_contact, ic );
505       
506        code = g_strdup( msg );
507        g_strstrip( code );
508        ret = oauth2_access_token( jd->oauth2_service, OAUTH2_AUTH_CODE,
509                                   code, sasl_oauth2_got_token, ic );
510       
511        g_free( code );
512        return ret;
513}
514
515int sasl_oauth2_refresh( struct im_connection *ic, const char *refresh_token )
516{
517        struct jabber_data *jd = ic->proto_data;
518       
519        return oauth2_access_token( jd->oauth2_service, OAUTH2_AUTH_REFRESH,
520                                    refresh_token, sasl_oauth2_got_token, ic );
521}
522
523static void sasl_oauth2_got_token( gpointer data, const char *access_token, const char *refresh_token )
524{
525        struct im_connection *ic = data;
526        struct jabber_data *jd;
527       
528        if( g_slist_find( jabber_connections, ic ) == NULL )
529                return;
530       
531        jd = ic->proto_data;
532       
533        if( access_token == NULL )
534        {
535                imcb_error( ic, "OAuth failure (missing access token)" );
536                imc_logout( ic, TRUE );
537                return;
538        }
539        if( refresh_token != NULL )
540        {
541                g_free( ic->acc->pass );
542                ic->acc->pass = g_strdup_printf( "refresh_token=%s", refresh_token );
543        }
544       
545        g_free( jd->oauth2_access_token );
546        jd->oauth2_access_token = g_strdup( access_token );
547       
548        jabber_connect( ic );
549}
Note: See TracBrowser for help on using the repository browser.