New method for OAuth2 authentication and receiving of access token from David Fletche...
authorMichael Rasmussen <mir@datanom.net>
Fri, 20 May 2022 19:31:35 +0000 (21:31 +0200)
committerMichael Rasmussen <mir@datanom.net>
Fri, 20 May 2022 19:31:35 +0000 (21:31 +0200)
Signed-off-by: Michael Rasmussen <mir@datanom.net>
src/oauth2.c
src/prefs_account.c

index e760eccfa97c21b2bf7bdf85e2361414ddda283a..1f7f599595bfa047d9b1a282f07e1c947e899ae9 100644 (file)
@@ -47,7 +47,7 @@ static gchar *OAUTH2info[4][17]={
   {"accounts.google.com",
    "",
    ".",
-   "urn:ietf:wg:oauth:2.0:oob",
+   "http://127.0.0.1:8888",
    "/o/oauth2/auth",
    "/o/oauth2/token",
    "/o/oauth2/token",
@@ -64,7 +64,7 @@ static gchar *OAUTH2info[4][17]={
   {"login.microsoftonline.com",
    "",
    "",
-   "https://login.microsoftonline.com/common/oauth2/nativeclient",
+   "http://127.0.0.1:8888",
    "/common/oauth2/v2.0/authorize",
    "/common/oauth2/v2.0/token",
    "/common/oauth2/v2.0/token",
@@ -76,12 +76,12 @@ static gchar *OAUTH2info[4][17]={
    "",
    "offline",
    "wl.imap offline_access",
-   "fragment",
+   "query",
    ""},
   {"login.microsoftonline.com",
    "",
    "",
-   "https://login.microsoftonline.com/common/oauth2/nativeclient",
+   "http://127.0.0.1:8888",
    "/common/oauth2/v2.0/authorize",
    "/common/oauth2/v2.0/token",
    "/common/oauth2/v2.0/token",
@@ -93,7 +93,7 @@ static gchar *OAUTH2info[4][17]={
    "",
    "offline",
    "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send",
-   "fragment",
+   "query",
    ""},
   {"api.login.yahoo.com",
    "",
@@ -116,9 +116,9 @@ static gchar *OAUTH2info[4][17]={
 
 static gchar *OAUTH2CodeMarker[5][2] = {
     {"",""},
-    {"google_begin_mark","google_end_mark"}, /* Not used since token avalable to user to copy in browser window */
-    {"#code=","&session_state"},
-    {"#code=","&session_state"},
+    {"code=","&scope="},
+    {"code="," HTTP"},
+    {"code="," HTTP"},
     {"yahoo_begin_mark","yahoo_end_mark"} /* Not used since token avalable to user to copy in browser window */
 };
 
@@ -191,9 +191,9 @@ static gint oauth2_filter_refresh (gchar *json, gchar *refresh_token)
 
 static gchar* oauth2_get_token_from_response(Oauth2Service provider, const gchar* response) {
        gchar* token = NULL;
-
+       
         debug_print("Auth response: %s\n", response);
-        if (provider == OAUTH2AUTH_YAHOO || provider == OAUTH2AUTH_GOOGLE) {
+        if (provider == OAUTH2AUTH_YAHOO) {
                 /* Providers which display auth token in browser for users to copy */
                 token = g_strdup(response);
         } else {
@@ -206,7 +206,7 @@ static gchar* oauth2_get_token_from_response(Oauth2Service provider, const gchar
                         return NULL;
                 token = g_strndup(start, stop - start);
         }
-
+       
        return token;
 }
 
@@ -225,8 +225,8 @@ int oauth2_obtain_tokens (Oauth2Service provider, OAUTH2Data *OAUTH2Data, const
        SockInfo *sock;
        gchar *client_id;
        gchar *client_secret;
-    gchar *token = NULL;
-    gchar *tmp;
+        gchar *token = NULL;
+        gchar *tmp;
        gint i;
 
        i = (int)provider - 1;
@@ -239,7 +239,7 @@ int oauth2_obtain_tokens (Oauth2Service provider, OAUTH2Data *OAUTH2Data, const
                 log_message(LOG_PROTOCOL, _("OAuth2 missing authorization code\n"));
                 return (1);
         }
-
+        debug_print("Connect: %s:443\n", OAUTH2info[i][OA2_BASE_URL]);
        sock = sock_connect(OAUTH2info[i][OA2_BASE_URL], 443);
        if (sock == NULL) {
                 log_message(LOG_PROTOCOL, _("OAuth2 connection error\n"));
@@ -267,12 +267,9 @@ int oauth2_obtain_tokens (Oauth2Service provider, OAUTH2Data *OAUTH2Data, const
        else
          client_id = oauth2_decode(OAUTH2info[i][OA2_CLIENT_ID]);
 
-       uri = g_uri_escape_string (client_id, NULL, FALSE);
-       uri2 = g_uri_escape_string (token, NULL, FALSE);
-       body = g_strconcat ("client_id=", uri, "&code=", uri2, NULL);
-    g_free(uri2);
-    g_free(uri);
-    g_free(token);
+        body = g_strconcat ("client_id=", client_id, "&code=", token, NULL);
+        debug_print("Body: %s\n", body);
+        g_free(token);
 
        if(OAUTH2info[i][OA2_CLIENT_SECRET][0]){
          //Only allow custom client secret if the service provider would usually expect a client secret
@@ -283,45 +280,35 @@ int oauth2_obtain_tokens (Oauth2Service provider, OAUTH2Data *OAUTH2Data, const
          uri = g_uri_escape_string (client_secret, NULL, FALSE);
          tmp = g_strconcat (body, "&client_secret=", uri, NULL);
          g_free(body);
-      g_free(uri);
+          g_free(uri);
          body = tmp;
        }else{
          client_secret = g_strconcat ("", NULL);
        }
 
        if(OAUTH2info[i][OA2_REDIRECT_URI][0]) {
-         uri = g_uri_escape_string (OAUTH2info[i][OA2_REDIRECT_URI], NULL, FALSE);
-         tmp = g_strconcat (body, "&redirect_uri=", uri, NULL);
+          tmp = g_strconcat(body, "&redirect_uri=", OAUTH2info[i][OA2_REDIRECT_URI], NULL);
          g_free(body);
-         g_free(uri);
          body = tmp;
        }
        if(OAUTH2info[i][OA2_GRANT_TYPE_ACCESS][0]) {
-         uri = g_uri_escape_string (OAUTH2info[i][OA2_GRANT_TYPE_ACCESS], NULL, FALSE);
-         tmp = g_strconcat (body, "&grant_type=", uri, NULL);
+          tmp = g_strconcat(body, "&grant_type=", OAUTH2info[i][OA2_GRANT_TYPE_ACCESS], NULL);
          g_free(body);
-         g_free(uri);
          body = tmp;
        }
        if(OAUTH2info[i][OA2_TENANT][0]) {
-         uri = g_uri_escape_string (OAUTH2info[i][OA2_TENANT], NULL, FALSE);
-         tmp = g_strconcat (body, "&tenant=", uri, NULL);
+          tmp = g_strconcat(body, "&tenant=", OAUTH2info[i][OA2_TENANT], NULL);
          g_free(body);
-         g_free(uri);
          body = tmp;
        }
        if(OAUTH2info[i][OA2_SCOPE_FOR_ACCESS][0]) {
-         uri = g_uri_escape_string (OAUTH2info[i][OA2_SCOPE_FOR_ACCESS], NULL, FALSE);
-         tmp = g_strconcat (body, "&scope=", uri, NULL);
+          tmp = g_strconcat(body, "&scope=", OAUTH2info[i][OA2_SCOPE_FOR_ACCESS], NULL);
          g_free(body);
-         g_free(uri);
          body = tmp;
        }
        if(OAUTH2info[i][OA2_STATE][0]) {
-         uri = g_uri_escape_string (OAUTH2info[i][OA2_STATE], NULL, FALSE);
-         tmp = g_strconcat (body, "&state=", uri, NULL);
+          tmp = g_strconcat(body, "&state=", OAUTH2info[i][OA2_STATE], NULL);
          g_free(body);
-         g_free(uri);
          body = tmp;
        }
 
index 7e84ec7a365f54840aa3ad47c9d7d5f581ad210c..0f94467460c7abf8296897d254ad5d513ff1dc61 100644 (file)
@@ -68,6 +68,7 @@
 #ifdef USE_GNUTLS
 #include <gnutls/gnutls.h>
 #endif
+#include <pthread.h> 
 
 static gboolean cancelled;
 static gboolean new_account;
@@ -95,6 +96,9 @@ struct AutocheckWidgets {
 
 static GSList *prefs_pages = NULL;
 
+static pthread_t oauth2_listener_tid;
+static int oauth2_listener_cancel = 0;
+
 typedef struct BasicPage
 {
     PrefsPage page;
@@ -422,6 +426,8 @@ static void prefs_account_oauth2_provider_set_data_from_optmenu
 static void prefs_account_oauth2_provider_set_optmenu  (PrefParam *pparam);
 static void prefs_account_oauth2_copy_url                       (GtkButton *button, 
                                                         gpointer data);
+static void * prefs_account_oauth2_listener(void *params);
+static int  prefs_account_oauth2_get_line(int sock, char *buf, int size);
 static void prefs_account_oauth2_set_sensitivity(void);
 static void prefs_account_oauth2_set_auth_sensitivity(void);
 static void prefs_account_oauth2_obtain_tokens(GtkButton *button, gpointer data);
@@ -3776,6 +3782,13 @@ static void send_destroy_widget_func(PrefsPage *_page)
 static void oauth2_destroy_widget_func(PrefsPage *_page)
 {
        /* Oauth2Page *page = (Oauth2Page *) _page; */
+
+        if(oauth2_listener_tid){
+         debug_print("Closing oauth2 listener thread\n");
+         oauth2_listener_cancel = 1;
+         pthread_join(oauth2_listener_tid, NULL);
+         oauth2_listener_tid = (pthread_t)NULL;
+       }
 }
 
 static void compose_destroy_widget_func(PrefsPage *_page)
@@ -4105,6 +4118,8 @@ static void register_oauth2_page(void)
        oauth2_page.page.save_page = oauth2_save_func;
        oauth2_page.page.can_close = oauth2_can_close_func;
 
+       oauth2_listener_tid = (pthread_t)NULL;
+
        prefs_account_register_page((PrefsPage *) &oauth2_page);
 }
 
@@ -5113,6 +5128,22 @@ static void prefs_account_oauth2_copy_url(GtkButton *button, gpointer data)
                open_uri(url, prefs_common_get_uri_cmd());
 
        g_free(url);
+
+       //Start listener for authorisation reply
+       //Needs to be in a separate thread to avoid hanging while we wait, and to allow cancellation of the process
+       //Avoid starting multiple threads if someone clicks this button more than once. 
+
+       //if thead exists cancel it here
+       if(oauth2_listener_tid){
+         debug_print("Cancelling old oauth2 listener thread\n");
+         oauth2_listener_cancel = 1;
+         pthread_join(oauth2_listener_tid, NULL);
+         oauth2_listener_tid = (pthread_t)NULL;
+       }
+       debug_print("Starting oauth2 listener thread\n");
+       oauth2_listener_cancel = 0;
+       pthread_create(&oauth2_listener_tid, NULL, prefs_account_oauth2_listener, (void*)win);
+       
 }
 
 static void prefs_account_oauth2_obtain_tokens(GtkButton *button, gpointer data)
@@ -5977,3 +6008,186 @@ static void prefs_account_receive_itv_spinbutton_value_changed_cb(GtkWidget *w,
                                PREFS_RECV_AUTOCHECK_MIN_INTERVAL);
        }
 }
+
+//Automation of the oauth2 authorisation process to receive loopback callback generated by redirect in browser
+static void * prefs_account_oauth2_listener(void * param)
+{ 
+        int socket_desc, client_sock, c;
+       struct sockaddr_in server , client;
+       char client_message[2000];
+       char reply[600];
+       char reply_message[400];
+       gchar *trim_text = NULL;
+       fd_set rfds;
+       gint ret;
+       struct timeval timeout;
+       struct BasicProtocol *protocol_optmenu = (struct BasicProtocol *)oauth2_page.protocol_optmenu;
+       GtkWidget *optmenu = protocol_optmenu->combobox;
+       Oauth2Service service;
+        OAUTH2Data *OAUTH2Data = g_malloc(sizeof(* OAUTH2Data));
+
+       
+       //pthread_detach(pthread_self());
+       debug_print("oauth2 listener thread running\n");
+     
+       //Create socket
+       socket_desc = socket(AF_INET , SOCK_STREAM , 0);
+       if (socket_desc == -1)
+       {
+               debug_print("oauth2 listener could not create socket\n");
+               return NULL;
+       }
+       debug_print("oauth2 listener socket created\n");
+       
+       //Prepare the sockaddr_in structure
+       server.sin_family = AF_INET;
+       server.sin_addr.s_addr = INADDR_ANY;
+       server.sin_port = htons( 8888 );
+       
+       //Bind
+       if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
+       {
+               debug_print("oauth2 listener bind failed\n");
+               return NULL;
+       }
+       debug_print("oauth2 listener bind done\n");
+
+       listen(socket_desc , 1);
+       
+       //Accept and incoming connection
+       debug_print("oauth2 listener waiting for incoming connections...\n");
+       c = sizeof(struct sockaddr_in);
+       
+       do{
+            FD_ZERO(&rfds);
+            FD_SET(socket_desc, &rfds);
+            timeout.tv_sec = 1;
+            timeout.tv_usec = 0;
+
+            select(socket_desc+1, &rfds, NULL, NULL, &timeout);
+            
+            //select woke up, maybe accept connection from an incoming client
+            if(FD_ISSET(socket_desc, &rfds)){
+              
+                 client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c);
+                 if (client_sock < 0){
+                   debug_print("oauth2 listener accept failed\n");
+                   return NULL;
+                 }
+                 debug_print("oauth2 listener connection accepted\n");
+                 
+                 //Receive message sent to the loopback address by the authorisation page
+                 prefs_account_oauth2_get_line(client_sock, client_message, sizeof(client_message));
+                 trim_text = g_strdup(client_message);
+                 g_strstrip(trim_text);
+
+                 gtk_entry_set_text(GTK_ENTRY(oauth2_page.oauth2_authcode_entry), trim_text != NULL ? trim_text : "");
+                 gtk_widget_set_sensitive(oauth2_page.oauth2_authcode_entry, FALSE);
+                 gtk_widget_set_sensitive(oauth2_page.oauth2_authorise_btn, FALSE);
+                 
+                 oauth2_init (OAUTH2Data);
+                 
+                 OAUTH2Data->custom_client_secret = 
+                   g_strdup(gtk_entry_get_text((GtkEntry *)oauth2_page.oauth2_client_secret_entry));
+                 OAUTH2Data->custom_client_id = 
+                   g_strdup(gtk_entry_get_text((GtkEntry *)oauth2_page.oauth2_client_id_entry));
+                 
+                 service = combobox_get_active_data(GTK_COMBO_BOX(optmenu));
+                 ret = oauth2_obtain_tokens (service, OAUTH2Data, trim_text);
+                 
+                 if(!ret){
+                   if(OAUTH2Data->refresh_token != NULL){
+                     passwd_store_set_account(tmp_ac_prefs.account_id,
+                                              PWS_ACCOUNT_OAUTH2_REFRESH,
+                                              OAUTH2Data->refresh_token,
+                                              FALSE);
+                     log_message(LOG_PROTOCOL, "OAuth2 refresh token stored\n");
+                   }
+                   
+                   if(OAUTH2Data->access_token != NULL){
+                     passwd_store_set_account(tmp_ac_prefs.account_id,
+                                              PWS_ACCOUNT_RECV,
+                                              OAUTH2Data->access_token,
+                                              FALSE);
+                     
+                     passwd_store_set_account(tmp_ac_prefs.account_id,
+                                              PWS_ACCOUNT_SEND,
+                                              OAUTH2Data->access_token,
+                                              FALSE);
+                     log_message(LOG_PROTOCOL, "OAuth2 access token stored\n");
+                     
+                     gtk_entry_set_text(GTK_ENTRY(basic_page.pass_entry), OAUTH2Data->access_token);
+                     gtk_entry_set_text(GTK_ENTRY(send_page.smtp_pass_entry), OAUTH2Data->access_token);
+                   }
+                   
+                   if(OAUTH2Data->expiry_str != NULL){
+                     passwd_store_set_account(tmp_ac_prefs.account_id,
+                                              PWS_ACCOUNT_OAUTH2_EXPIRY,
+                                              OAUTH2Data->expiry_str,
+                                              FALSE);
+                     log_message(LOG_PROTOCOL, "OAuth2 access token expiry stored\n");
+                   }
+                   
+                   tmp_ac_prefs.oauth2_date = g_get_real_time () / G_USEC_PER_SEC;
+                   
+                   sprintf(reply_message, "<html><body><h1>Authorisation complete</h1><p>Your oauth2 authorisation code has been received by Claws Mail</p></body></html>");
+                 }else{
+                   //Something went wrong
+                   log_message(LOG_PROTOCOL, "oauth2 authorisation code not received\n");
+                   sprintf(reply_message, "<html><body><h1>Authorisation NOT completed</h1><p>Your authorisation code was not received by Claws Mail</p></body></html>");
+                 }
+                 
+                 sprintf(reply, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nContent-Length: %lu\r\n\r\n%s", strlen(reply_message), reply_message);
+                 write(client_sock, reply, strlen(reply));
+                 close(client_sock);
+            }
+            
+       }while(ret && !oauth2_listener_cancel);
+
+       close(socket_desc);
+       g_free(trim_text);
+       g_free(OAUTH2Data);
+       
+       //To Do
+       //(3) Work out how to give an indicataion in the main window that authentication has happened
+       //(6) Clean up, test, copy over to git version
+       //(1) Work out why extracting code from this causes segfault in oauth2_get_token_from_response
+       //http://127.0.0.1:8888/?code=M.R3_BAY.09a02423-7bf8-8e51-c4a3-5565f8e7d56b HTTP/1.1
+       return NULL;
+}
+
+
+
+
+
+static int prefs_account_oauth2_get_line(int sock, char *buf, int size)
+{
+       int i = 0;
+       char c = '\0';
+       int n;
+       
+       while ((i < size - 1) && (c != '\n'))
+         {
+           n = recv(sock, &c, 1, 0);
+           //printf("%02X\n", c);
+           if (n > 0)
+             {
+               if (c == '\r')
+                 {
+                   n = recv(sock, &c, 1, MSG_PEEK);
+                   //printf("%02X\n", c);
+                   if ((n > 0) && (c == '\n'))
+                     recv(sock, &c, 1, 0);
+                   else
+                     c = '\n';
+                 }
+               buf[i] = c;
+               i++;
+             }
+           else
+             c = '\n';
+         }
+       buf[i] = '\0';
+       
+       return(i);
+}