talons

Fork of Claws Mail https://www.claws-mail
Log | Files | Refs | README | LICENSE

commit e3a613af5ac6ed643b53f3b95f48cb4cb0b5c4e0
parent c23d02b23105ab05d0b0e1a037da7596e6c61777
Author: Michael Rasmussen <mir@datanom.net>
Date:   Fri, 20 May 2022 21:31:35 +0200

New method for OAuth2 authentication and receiving of access token from David Fletcher applied including my fix for Microsoft OAuth2 functionality

Signed-off-by: Michael Rasmussen <mir@datanom.net>

Diffstat:
Msrc/oauth2.c | 59+++++++++++++++++++++++------------------------------------
Msrc/prefs_account.c | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 237 insertions(+), 36 deletions(-)

diff --git a/src/oauth2.c b/src/oauth2.c @@ -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; } diff --git a/src/prefs_account.c b/src/prefs_account.c @@ -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); +}