res_xmpp: Google OAuth 2.0 protocol support for XMPP / Motif
authorAndrey Egorov <andr06@gmail.com>
Fri, 4 Aug 2017 14:25:52 +0000 (17:25 +0300)
committerJoshua Colp <jcolp@digium.com>
Tue, 15 Aug 2017 11:09:52 +0000 (06:09 -0500)
Add ability to use tokens instead of passwords according to Google OAuth 2.0
protocol.

ASTERISK-27169
Reported by: Andrey Egorov
Tested by: Andrey Egorov

Change-Id: I07f7052a502457ab55010a4d3686653b60f4c8db

CHANGES
configs/samples/xmpp.conf.sample
res/res_xmpp.c

diff --git a/CHANGES b/CHANGES
index f6c94f8..67454bc 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -73,6 +73,12 @@ res_musiconhold
  * New dialplan function PJSIP_DTMF_MODE added to get or change the DTMF mode
    of a channel on a per-call basis.
 
+res_xmpp
+-----------------
+ * OAuth 2.0 authentication is now supported when contacting Google. Follow the
+   instructions in xmpp.conf.sample to retrieve and configure the necessary
+   tokens.
+
 ------------------------------------------------------------------------------
 --- Functionality changes from Asterisk 14.5.0 to Asterisk 14.6.0 ------------
 ------------------------------------------------------------------------------
index dad0f79..e3a4be1 100644 (file)
 ;pubsub_node=pubsub.astjab.org         ; Node to use for publishing events via PubSub
 ;username=asterisk@astjab.org/asterisk ; Username with optional resource.
 ;secret=blah                           ; Password
+;refresh_token=TOKEN_VALUE             ; Refresh token issued by Google OAuth 2.0 protocol.
+                                       ; `secret` must NOT be set if you use OAuth.
+                                       ; See https://developers.google.com/identity/protocols/OAuth2WebServer
+                                       ; for more details.
+                                       ; For test reasons you can obtain one on the page
+                                       ; https://developers.google.com/oauthplayground/
+                                       ; 1. Click on Settings icon, check "Use your own OAuth credentials"
+                                       ;    and enter your Client ID and Client Secret (see below).
+                                       ; 2. Input the scope https://www.googleapis.com/auth/googletalk
+                                       ;    and push "Authorize APIs" button.
+                                       ; 3. Approve permissions.
+                                       ; 4. On section "Step 2" push "Exchange authorization code for tokens"
+                                       ;    and get your Refresh token.
+;oauth_clientid=OAUTH_CLIENT_ID_VALUE  ; The application's client id to authorize using Google OAuth 2.0 protocol.
+;oauth_secret=OAUTH_SECRET_VALUE       ; The application's client secret to authorize using Google OAuth 2.0 protocol.
+                                       ; 1. Create new Project on the page:
+                                       ;    https://console.cloud.google.com/apis/credentials/oauthclient
+                                       ; 2. Create new Application ID on the same page with type Web-application.
+                                       ;    In section "Allowed URI redirections" put the path to the corresponding
+                                       ;    script on your site or https://developers.google.com/oauthplayground
+                                       ;    if you would like to obtain refresh_token from users by hand
+                                       ;    (for example, for test reasons).
+                                       ; 3. Client ID and Client Secret will be shown and available on the same page.
 ;priority=1                            ; Resource priority
 ;port=5222                             ; Port to use defaults to 5222
 ;usetls=yes                            ; Use tls or not
index cc9d56f..f8eb505 100644 (file)
@@ -59,6 +59,7 @@
 #include "asterisk/manager.h"
 #include "asterisk/cli.h"
 #include "asterisk/config_options.h"
+#include "asterisk/json.h"
 
 /*** DOCUMENTATION
        <application name="JabberSend" language="en_US" module="res_xmpp">
                                <configOption name="secret">
                                        <synopsis>XMPP password</synopsis>
                                </configOption>
+                               <configOption name="refresh_token">
+                                       <synopsis>Google OAuth 2.0 refresh token</synopsis>
+                               </configOption>
+                               <configOption name="oauth_clientid">
+                                       <synopsis>Google OAuth 2.0 application's client id</synopsis>
+                               </configOption>
+                               <configOption name="oauth_secret">
+                                       <synopsis>Google OAuth 2.0 application's secret</synopsis>
+                               </configOption>
                                <configOption name="serverhost">
                                        <synopsis>Route to server, e.g. talk.google.com</synopsis>
                                </configOption>
@@ -459,6 +469,9 @@ struct ast_xmpp_client_config {
                AST_STRING_FIELD(name);        /*!< Name of the client connection */
                AST_STRING_FIELD(user);        /*!< Username to use for authentication */
                AST_STRING_FIELD(password);    /*!< Password to use for authentication */
+               AST_STRING_FIELD(refresh_token);   /*!< Refresh token to use for OAuth authentication */
+               AST_STRING_FIELD(oauth_clientid);  /*!< Client ID to use for OAuth authentication */
+               AST_STRING_FIELD(oauth_secret);    /*!< Secret to use for OAuth authentication */
                AST_STRING_FIELD(server);      /*!< Server hostname */
                AST_STRING_FIELD(statusmsg);   /*!< Status message for presence */
                AST_STRING_FIELD(pubsubnode);  /*!< Pubsub node */
@@ -527,6 +540,7 @@ static ast_cond_t message_received_condition;
 static ast_mutex_t messagelock;
 
 static int xmpp_client_config_post_apply(void *obj, void *arg, int flags);
+static int fetch_access_token(struct ast_xmpp_client_config *cfg);
 
 /*! \brief Destructor function for configuration */
 static void ast_xmpp_client_config_destructor(void *obj)
@@ -759,12 +773,16 @@ static int xmpp_config_prelink(void *newitem)
        if (ast_strlen_zero(clientcfg->user)) {
                ast_log(LOG_ERROR, "No user specified on client '%s'\n", clientcfg->name);
                return -1;
-       } else if (ast_strlen_zero(clientcfg->password)) {
-               ast_log(LOG_ERROR, "No password specified on client '%s'\n", clientcfg->name);
+       } else if (ast_strlen_zero(clientcfg->password) && ast_strlen_zero(clientcfg->refresh_token)) {
+               ast_log(LOG_ERROR, "No password or refresh_token specified on client '%s'\n", clientcfg->name);
                return -1;
        } else if (ast_strlen_zero(clientcfg->server)) {
                ast_log(LOG_ERROR, "No server specified on client '%s'\n", clientcfg->name);
                return -1;
+       } else if (!ast_strlen_zero(clientcfg->refresh_token) &&
+                  (ast_strlen_zero(clientcfg->oauth_clientid) || ast_strlen_zero(clientcfg->oauth_secret))) {
+               ast_log(LOG_ERROR, "No oauth_clientid or oauth_secret specified, so client '%s' can't be used\n", clientcfg->name);
+               return -1;
        }
 
        /* If this is a new connection force a reconnect */
@@ -776,6 +794,9 @@ static int xmpp_config_prelink(void *newitem)
        /* If any configuration options are changing that would require reconnecting set the bit so we will do so if possible */
        if (strcmp(clientcfg->user, oldclientcfg->user) ||
            strcmp(clientcfg->password, oldclientcfg->password) ||
+           strcmp(clientcfg->refresh_token, oldclientcfg->refresh_token) ||
+           strcmp(clientcfg->oauth_clientid, oldclientcfg->oauth_clientid) ||
+           strcmp(clientcfg->oauth_secret, oldclientcfg->oauth_secret) ||
            strcmp(clientcfg->server, oldclientcfg->server) ||
            (clientcfg->port != oldclientcfg->port) ||
            (ast_test_flag(&clientcfg->flags, XMPP_COMPONENT) != ast_test_flag(&oldclientcfg->flags, XMPP_COMPONENT)) ||
@@ -2784,7 +2805,13 @@ static int xmpp_client_authenticate_sasl(struct ast_xmpp_client *client, struct
        }
 
        iks_insert_attrib(auth, "xmlns", IKS_NS_XMPP_SASL);
-       iks_insert_attrib(auth, "mechanism", "PLAIN");
+       if (!ast_strlen_zero(cfg->refresh_token)) {
+               iks_insert_attrib(auth, "mechanism", "X-OAUTH2");
+               iks_insert_attrib(auth, "auth:service", "oauth2");
+               iks_insert_attrib(auth, "xmlns:auth", "http://www.google.com/talk/protocol/auth");
+       } else {
+               iks_insert_attrib(auth, "mechanism", "PLAIN");
+       }
 
        if (strchr(client->jid->user, '/')) {
                char *user = ast_strdupa(client->jid->user);
@@ -3283,28 +3310,28 @@ static int xmpp_ping_request(struct ast_xmpp_client *client, const char *to, con
 {
        iks *iq, *ping;
        int res;
-       
+
        ast_debug(2, "JABBER: Sending Keep-Alive Ping for client '%s'\n", client->name);
 
        if (!(iq = iks_new("iq")) || !(ping = iks_new("ping"))) {
                iks_delete(iq);
                return -1;
        }
-       
+
        iks_insert_attrib(iq, "type", "get");
        iks_insert_attrib(iq, "to", to);
        iks_insert_attrib(iq, "from", from);
-       
+
        ast_xmpp_client_lock(client);
        iks_insert_attrib(iq, "id", client->mid);
        ast_xmpp_increment_mid(client->mid);
        ast_xmpp_client_unlock(client);
-       
+
        iks_insert_attrib(ping, "xmlns", "urn:xmpp:ping");
        iks_insert_node(iq, ping);
-       
+
        res = ast_xmpp_client_send(client, iq);
-       
+
        iks_delete(ping);
        iks_delete(iq);
 
@@ -3625,6 +3652,13 @@ static int xmpp_client_reconnect(struct ast_xmpp_client *client)
                return -1;
        }
 
+       if (!ast_strlen_zero(clientcfg->refresh_token)) {
+               ast_debug(2, "Obtaining OAuth access token for client '%s'\n", client->name);
+               if (fetch_access_token(clientcfg)) {
+                       return -1;
+               }
+       }
+
        ast_xmpp_client_disconnect(client);
 
        client->timeout = 50;
@@ -3641,7 +3675,7 @@ static int xmpp_client_reconnect(struct ast_xmpp_client *client)
 
        /* Set socket timeout options */
        setsockopt(iks_fd(client->parser), SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,sizeof(struct timeval));
-       
+
        if (res == IKS_NET_NOCONN) {
                ast_log(LOG_ERROR, "No XMPP connection available when trying to connect client '%s'\n", client->name);
                return -1;
@@ -3726,7 +3760,7 @@ static int xmpp_client_receive(struct ast_xmpp_client *client, unsigned int time
                /* Log the message here, because iksemel's logHook is
                   unaccessible */
                xmpp_log_hook(client, buf, len, 1);
-               
+
                if(buf[0] == ' ') {
                        ast_debug(1, "JABBER: Detected Google Keep Alive. "
                                "Sending out Ping request for client '%s'\n", client->name);
@@ -3867,6 +3901,42 @@ static int xmpp_client_config_merge_buddies(void *obj, void *arg, int flags)
        return 1;
 }
 
+static int fetch_access_token(struct ast_xmpp_client_config *cfg)
+{
+       RAII_VAR(char *, cmd, NULL, ast_free);
+       char cBuf[1024] = "";
+       const char *url = "https://www.googleapis.com/oauth2/v3/token";
+       struct ast_json_error error;
+       RAII_VAR(struct ast_json *, jobj, NULL, ast_json_unref);
+
+       ast_asprintf(&cmd, "CURL(%s,client_id=%s&client_secret=%s&refresh_token=%s&grant_type=refresh_token)",
+                    url, cfg->oauth_clientid, cfg->oauth_secret, cfg->refresh_token);
+
+       ast_debug(2, "Performing OAuth 2.0 authentication for client '%s' using command: %s\n",
+               cfg->name, cmd);
+
+       if (!ast_func_read(NULL, cmd, cBuf, sizeof(cBuf) - 1)) {
+               ast_log(LOG_ERROR, "CURL is unavailable. This is required for OAuth 2.0 authentication of XMPP client '%s'. Please ensure it is loaded.\n",
+                       cfg->name);
+               return -1;
+       }
+
+       ast_debug(2, "OAuth 2.0 authentication for client '%s' returned: %s\n", cfg->name, cBuf);
+
+       jobj = ast_json_load_string(cBuf, &error);
+       if (jobj) {
+               const char *token = ast_json_string_get(ast_json_object_get(jobj, "access_token"));
+               if (token) {
+                       ast_string_field_set(cfg, password, token);
+                       return 0;
+               }
+       }
+
+       ast_log(LOG_ERROR, "An error occurred while performing OAuth 2.0 authentication for client '%s': %s\n", cfg->name, cBuf);
+
+       return -1;
+}
+
 static int xmpp_client_config_post_apply(void *obj, void *arg, int flags)
 {
        struct ast_xmpp_client_config *cfg = obj;
@@ -4620,8 +4690,8 @@ static int client_buddy_handler(const struct aco_option *opt, struct ast_variabl
  * Module loading including tests for configuration or dependencies.
  * This function can return AST_MODULE_LOAD_FAILURE, AST_MODULE_LOAD_DECLINE,
  * or AST_MODULE_LOAD_SUCCESS. If a dependency or environment variable fails
- * tests return AST_MODULE_LOAD_FAILURE. If the module can not load the 
- * configuration file or other non-critical problem return 
+ * tests return AST_MODULE_LOAD_FAILURE. If the module can not load the
+ * configuration file or other non-critical problem return
  * AST_MODULE_LOAD_DECLINE. On success return AST_MODULE_LOAD_SUCCESS.
  */
 static int load_module(void)
@@ -4639,6 +4709,9 @@ static int load_module(void)
 
        aco_option_register(&cfg_info, "username", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, user));
        aco_option_register(&cfg_info, "secret", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, password));
+       aco_option_register(&cfg_info, "refresh_token", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, refresh_token));
+       aco_option_register(&cfg_info, "oauth_clientid", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, oauth_clientid));
+       aco_option_register(&cfg_info, "oauth_secret", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, oauth_secret));
        aco_option_register(&cfg_info, "serverhost", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, server));
        aco_option_register(&cfg_info, "statusmessage", ACO_EXACT, client_options, "Online and Available", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, statusmsg));
        aco_option_register(&cfg_info, "pubsub_node", ACO_EXACT, client_options, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_xmpp_client_config, pubsubnode));