2 * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 2021 the Claws Mail team
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "claws-features.h"
27 #include <glib/gi18n.h>
41 #include "common/passcrypt.h"
43 //Yahoo requires token requests to send POST header Authorization: Basic
44 //where the password is Base64 encoding of client_id:client_secret
46 static gchar
*OAUTH2info
[4][17]={
47 {"accounts.google.com",
50 "urn:ietf:wg:oauth:2.0:oob",
55 "https://mail.google.com",
64 {"login.microsoftonline.com",
67 "https://login.microsoftonline.com/common/oauth2/nativeclient",
68 "/common/oauth2/v2.0/authorize",
69 "/common/oauth2/v2.0/token",
70 "/common/oauth2/v2.0/token",
72 "wl.imap offline_access",
78 "wl.imap offline_access",
81 {"login.microsoftonline.com",
84 "https://login.microsoftonline.com/common/oauth2/nativeclient",
85 "/common/oauth2/v2.0/authorize",
86 "/common/oauth2/v2.0/token",
87 "/common/oauth2/v2.0/token",
89 "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send",
95 "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send",
98 {"api.login.yahoo.com",
102 "/oauth2/request_auth",
107 "authorization_code",
117 static gchar
*OAUTH2CodeMarker
[5][2] = {
119 {"google_begin_mark","google_end_mark"}, /* Not used since token avalable to user to copy in browser window */
120 {"#code=","&session_state"},
121 {"#code=","&session_state"},
122 {"yahoo_begin_mark","yahoo_end_mark"} /* Not used since token avalable to user to copy in browser window */
125 static gint
oauth2_post_request (gchar
*buf
, gchar
*host
, gchar
*resource
, gchar
*header
, gchar
*body
);
126 static gint
oauth2_filter_refresh (gchar
*json
, gchar
*refresh_token
);
127 static gint
oauth2_filter_access (gchar
*json
, gchar
*access_token
, gint
*expiry
);
128 static gint
oauth2_contact_server (SockInfo
*sock
, gchar
*request
, gchar
*response
);
131 static gint
oauth2_post_request (gchar
*buf
, gchar
*host
, gchar
*resource
, gchar
*header
, gchar
*body
)
137 return snprintf(buf
, OAUTH2BUFSIZE
, "POST %s HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: text/html,application/json\r\nContent-Length: %i\r\nHost: %s\r\nConnection: close\r\nUser-Agent: ClawsMail\r\n%s\r\n\r\n%s", resource
, len
, host
, header
, body
);
139 return snprintf(buf
, OAUTH2BUFSIZE
, "POST %s HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: text/html,application/json\r\nContent-Length: %i\r\nHost: %s\r\nConnection: close\r\nUser-Agent: ClawsMail\r\n\r\n%s", resource
, len
, host
, body
);
142 static gint
oauth2_filter_access (gchar
*json
, gchar
*access_token
, gint
*expiry
)
144 GMatchInfo
*matchInfo
;
147 regex
= g_regex_new ("\"access_token\": ?\"(.*?)\",?", 0, 0, NULL
);
148 g_regex_match (regex
, json
, 0, &matchInfo
);
149 if (g_match_info_matches (matchInfo
))
150 g_stpcpy (access_token
,g_match_info_fetch (matchInfo
, 1));
152 g_match_info_free (matchInfo
);
156 g_match_info_free (matchInfo
);
158 regex
= g_regex_new ("\"expires_in\": ?([0-9]*),?", 0, 0, NULL
);
159 g_regex_match (regex
, json
, 0, &matchInfo
);
160 if (g_match_info_matches (matchInfo
)){
161 // Reduce available token life to avoid attempting connections with (near) expired tokens
162 *expiry
= (g_get_real_time () / G_USEC_PER_SEC
) + atoi(g_match_info_fetch (matchInfo
, 1)) - 120;
164 g_match_info_free (matchInfo
);
168 g_match_info_free (matchInfo
);
173 static gint
oauth2_filter_refresh (gchar
*json
, gchar
*refresh_token
)
175 GMatchInfo
*matchInfo
;
178 regex
= g_regex_new ("\"refresh_token\": ?\"(.*?)\",?", 0, 0, NULL
);
179 g_regex_match (regex
, json
, 0, &matchInfo
);
180 if (g_match_info_matches (matchInfo
))
181 g_stpcpy (refresh_token
,g_match_info_fetch (matchInfo
, 1));
183 g_match_info_free (matchInfo
);
187 g_match_info_free (matchInfo
);
192 static gchar
* oauth2_get_token_from_response(Oauth2Service provider
, const gchar
* response
) {
195 debug_print("Auth response: %s\n", response
);
196 if (provider
== OAUTH2AUTH_YAHOO
|| provider
== OAUTH2AUTH_GOOGLE
) {
197 /* Providers which display auth token in browser for users to copy */
198 token
= g_strdup(response
);
200 gchar
* start
= g_strstr_len(response
, strlen(response
), OAUTH2CodeMarker
[provider
][0]);
201 start
+= strlen(OAUTH2CodeMarker
[provider
][0]);
204 gchar
* stop
= g_strstr_len(response
, strlen(response
), OAUTH2CodeMarker
[provider
][1]);
207 token
= g_strndup(start
, stop
- start
);
213 int oauth2_obtain_tokens (Oauth2Service provider
, OAUTH2Data
*OAUTH2Data
, const gchar
*authcode
)
221 gchar
*refresh_token
;
226 gchar
*client_secret
;
231 i
= (int)provider
- 1;
232 if (i
< 0 || i
> (OAUTH2AUTH_LAST
-1))
235 token
= oauth2_get_token_from_response(provider
, authcode
);
236 debug_print("Auth token: %s\n", token
);
238 log_message(LOG_PROTOCOL
, _("OAuth2 missing authorization code\n"));
242 sock
= sock_connect(OAUTH2info
[i
][OA2_BASE_URL
], 443);
244 log_message(LOG_PROTOCOL
, _("OAuth2 connection error\n"));
248 sock
->ssl_cert_auto_accept
= TRUE
;
249 sock_set_nonblocking_mode(sock
, FALSE
);
250 sock_set_io_timeout(10);
251 sock
->gnutls_priority
= "NORMAL:!VERS-SSL3.0:!VERS-TLS1.0:!VERS-TLS1.1";
252 if (ssl_init_socket(sock
) == FALSE
) {
253 log_message(LOG_PROTOCOL
, _("OAuth2 SSL/TLS connection error\n"));
258 refresh_token
= g_malloc(OAUTH2BUFSIZE
+1);
259 access_token
= g_malloc(OAUTH2BUFSIZE
+1);
260 request
= g_malloc(OAUTH2BUFSIZE
+1);
261 response
= g_malloc(OAUTH2BUFSIZE
+1);
262 body
= g_malloc(OAUTH2BUFSIZE
+1);
264 if(OAUTH2Data
->custom_client_id
)
265 client_id
= g_strdup(OAUTH2Data
->custom_client_id
);
267 client_id
= oauth2_decode(OAUTH2info
[i
][OA2_CLIENT_ID
]);
269 body
= g_strconcat ("client_id=", g_uri_escape_string (client_id
, NULL
, FALSE
),
270 "&code=",g_uri_escape_string (token
, NULL
, FALSE
), NULL
);
273 if(OAUTH2info
[i
][OA2_CLIENT_SECRET
][0]){
274 //Only allow custom client secret if the service provider would usually expect a client secret
275 if(OAUTH2Data
->custom_client_secret
)
276 client_secret
= g_strdup(OAUTH2Data
->custom_client_secret
);
278 client_secret
= oauth2_decode(OAUTH2info
[i
][OA2_CLIENT_SECRET
]);
279 body
= g_strconcat (body
, "&client_secret=", g_uri_escape_string (client_secret
, NULL
, FALSE
), NULL
);
281 client_secret
= g_strconcat ("", NULL
);
284 if(OAUTH2info
[i
][OA2_REDIRECT_URI
][0])
285 body
= g_strconcat (body
, "&redirect_uri=",g_uri_escape_string (OAUTH2info
[i
][OA2_REDIRECT_URI
], NULL
, FALSE
), NULL
);
286 if(OAUTH2info
[i
][OA2_GRANT_TYPE_ACCESS
][0])
287 body
= g_strconcat (body
, "&grant_type=", g_uri_escape_string (OAUTH2info
[i
][OA2_GRANT_TYPE_ACCESS
], NULL
, FALSE
), NULL
);
288 if(OAUTH2info
[i
][OA2_TENANT
][0])
289 body
= g_strconcat (body
, "&tenant=", g_uri_escape_string (OAUTH2info
[i
][OA2_TENANT
], NULL
, FALSE
), NULL
);
290 if(OAUTH2info
[i
][OA2_SCOPE_FOR_ACCESS
][0])
291 body
= g_strconcat (body
, "&scope=", g_uri_escape_string (OAUTH2info
[i
][OA2_SCOPE_FOR_ACCESS
], NULL
, FALSE
), NULL
);
292 if(OAUTH2info
[i
][OA2_STATE
][0])
293 body
= g_strconcat (body
, "&state=", g_uri_escape_string (OAUTH2info
[i
][OA2_STATE
], NULL
, FALSE
), NULL
);
295 if(OAUTH2info
[i
][OA2_HEADER_AUTH_BASIC
][0]){
296 tmp_hd
= g_strconcat(client_id
, ":", client_secret
, NULL
);
297 header
= g_strconcat ("Authorization: Basic ", g_base64_encode (tmp_hd
, strlen(tmp_hd
)), NULL
);
300 header
= g_strconcat ("", NULL
);
303 oauth2_post_request (request
, OAUTH2info
[i
][OA2_BASE_URL
], OAUTH2info
[i
][OA2_ACCESS_RESOURCE
], header
, body
);
304 ret
= oauth2_contact_server (sock
, request
, response
);
306 if(oauth2_filter_access (response
, access_token
, &expiry
) == 0){
307 OAUTH2Data
->access_token
= access_token
;
308 OAUTH2Data
->expiry
= expiry
;
309 OAUTH2Data
->expiry_str
= g_strdup_printf ("%i", expiry
);
311 log_message(LOG_PROTOCOL
, _("OAuth2 access token obtained\n"));
313 log_message(LOG_PROTOCOL
, _("OAuth2 access token not obtained\n"));
314 debug_print("OAuth2 - request: %s\n Response: %s", request
, response
);
318 if(oauth2_filter_refresh (response
, refresh_token
) == 0){
319 OAUTH2Data
->refresh_token
= refresh_token
;
320 log_message(LOG_PROTOCOL
, _("OAuth2 refresh token obtained\n"));
322 log_message(LOG_PROTOCOL
, _("OAuth2 refresh token not obtained\n"));
325 sock_close(sock
, TRUE
);
331 g_free(client_secret
);
336 gint
oauth2_use_refresh_token (Oauth2Service provider
, OAUTH2Data
*OAUTH2Data
)
349 gchar
*client_secret
;
353 i
= (int)provider
- 1;
354 if (i
< 0 || i
> (OAUTH2AUTH_LAST
-1))
357 sock
= sock_connect(OAUTH2info
[i
][OA2_BASE_URL
], 443);
359 log_message(LOG_PROTOCOL
, _("OAuth2 connection error\n"));
362 sock
->ssl_cert_auto_accept
= TRUE
;
363 sock_set_nonblocking_mode(sock
, FALSE
);
364 sock_set_io_timeout(10);
365 sock
->gnutls_priority
= "NORMAL:!VERS-SSL3.0:!VERS-TLS1.0:!VERS-TLS1.1";
366 if (ssl_init_socket(sock
) == FALSE
) {
367 log_message(LOG_PROTOCOL
, _("OAuth2 SSL connection error\n"));
371 access_token
= g_malloc(OAUTH2BUFSIZE
+1);
372 request
= g_malloc(OAUTH2BUFSIZE
+1);
373 response
= g_malloc(OAUTH2BUFSIZE
+1);
374 body
= g_malloc(OAUTH2BUFSIZE
+1);
376 if(OAUTH2Data
->custom_client_id
)
377 client_id
= g_strdup(OAUTH2Data
->custom_client_id
);
379 client_id
= oauth2_decode(OAUTH2info
[i
][OA2_CLIENT_ID
]);
381 body
= g_strconcat ("client_id=", g_uri_escape_string (client_id
, NULL
, FALSE
),
382 "&refresh_token=",OAUTH2Data
->refresh_token
, NULL
);
384 if(OAUTH2info
[i
][OA2_CLIENT_SECRET
][0]){
385 //Only allow custom client secret if the service provider would usually expect a client secret
386 if(OAUTH2Data
->custom_client_secret
)
387 client_secret
= g_strdup(OAUTH2Data
->custom_client_secret
);
389 client_secret
= oauth2_decode(OAUTH2info
[i
][OA2_CLIENT_SECRET
]);
390 body
= g_strconcat (body
, "&client_secret=", g_uri_escape_string (client_secret
, NULL
, FALSE
), NULL
);
392 client_secret
= g_strconcat ("", NULL
);
395 if(OAUTH2info
[i
][OA2_GRANT_TYPE_REFRESH
][0])
396 body
= g_strconcat (body
, "&grant_type=", g_uri_escape_string (OAUTH2info
[i
][OA2_GRANT_TYPE_REFRESH
], NULL
, FALSE
), NULL
);
397 if(OAUTH2info
[i
][OA2_SCOPE_FOR_ACCESS
][0])
398 body
= g_strconcat (body
, "&scope=", g_uri_escape_string (OAUTH2info
[i
][OA2_SCOPE_FOR_ACCESS
], NULL
, FALSE
), NULL
);
399 if(OAUTH2info
[i
][OA2_STATE
][0])
400 body
= g_strconcat (body
, "&state=", g_uri_escape_string (OAUTH2info
[i
][OA2_STATE
], NULL
, FALSE
), NULL
);
402 if(OAUTH2info
[i
][OA2_HEADER_AUTH_BASIC
][0]){
403 tmp_hd
= g_strconcat(client_id
, ":", client_secret
, NULL
);
404 header
= g_strconcat ("Authorization: Basic ", g_base64_encode (tmp_hd
, strlen(tmp_hd
)), NULL
);
407 header
= g_strconcat ("", NULL
);
410 oauth2_post_request (request
, OAUTH2info
[i
][OA2_BASE_URL
], OAUTH2info
[i
][OA2_REFRESH_RESOURCE
], header
, body
);
411 ret
= oauth2_contact_server (sock
, request
, response
);
413 if(oauth2_filter_access (response
, access_token
, &expiry
) == 0){
414 OAUTH2Data
->access_token
= access_token
;
415 OAUTH2Data
->expiry
= expiry
;
416 OAUTH2Data
->expiry_str
= g_strdup_printf ("%i", expiry
);
418 log_message(LOG_PROTOCOL
, _("OAuth2 access token obtained\n"));
420 log_message(LOG_PROTOCOL
, _("OAuth2 access token not obtained\n"));
421 debug_print("OAuth2 - request: %s\n Response: %s", request
, response
);
425 debug_print("OAuth2 - access token: %s\n", access_token
);
426 debug_print("OAuth2 - access token expiry: %i\n", expiry
);
428 sock_close(sock
, TRUE
);
434 g_free(client_secret
);
439 static gint
oauth2_contact_server (SockInfo
*sock
, gchar
*request
, gchar
*response
)
444 gint toread
= OAUTH2BUFSIZE
;
445 time_t startplus
= time(NULL
);
447 len
= strlen(request
);
451 if (sock_write (sock
, request
, len
+1) < 0) {
452 log_message(LOG_PROTOCOL
, _("OAuth2 socket write error\n"));
456 token
= g_strconcat ("", NULL
);
459 ret
= sock_read (sock
, response
, OAUTH2BUFSIZE
);
460 if (ret
< 0 && errno
== EAGAIN
)
468 token
= g_strconcat(token
, response
, NULL
);
469 } while ((toread
> 0) && (time(NULL
) < startplus
));
471 if(time(NULL
) >= startplus
)
472 log_message(LOG_PROTOCOL
, _("OAuth2 socket timeout error\n"));
479 gint
oauth2_authorisation_url (Oauth2Service provider
, gchar
**url
, const gchar
*custom_client_id
)
482 const gchar
*client_id
;
484 i
= (int)provider
- 1;
485 if (i
< 0 || i
> (OAUTH2AUTH_LAST
-1))
489 client_id
= custom_client_id
;
491 client_id
= oauth2_decode(OAUTH2info
[i
][OA2_CLIENT_ID
]);
493 *url
= g_strconcat ("https://", OAUTH2info
[i
][OA2_BASE_URL
],OAUTH2info
[i
][OA2_AUTH_RESOURCE
], "?client_id=",
494 g_uri_escape_string (client_id
, NULL
, FALSE
), NULL
);
496 if(OAUTH2info
[i
][OA2_REDIRECT_URI
][0])
497 *url
= g_strconcat (*url
, "&redirect_uri=", g_uri_escape_string (OAUTH2info
[i
][OA2_REDIRECT_URI
], NULL
, FALSE
), NULL
);
498 if(OAUTH2info
[i
][OA2_RESPONSE_TYPE
][0])
499 *url
= g_strconcat (*url
, "&response_type=",g_uri_escape_string (OAUTH2info
[i
][OA2_RESPONSE_TYPE
], NULL
, FALSE
), NULL
);
500 if(OAUTH2info
[i
][OA2_SCOPE_FOR_AUTH
][0])
501 *url
= g_strconcat (*url
, "&scope=", g_uri_escape_string (OAUTH2info
[i
][OA2_SCOPE_FOR_AUTH
], NULL
, FALSE
), NULL
);
502 if(OAUTH2info
[i
][OA2_TENANT
][0])
503 *url
= g_strconcat (*url
, "&tenant=", g_uri_escape_string (OAUTH2info
[i
][OA2_TENANT
], NULL
, FALSE
), NULL
);
504 if(OAUTH2info
[i
][OA2_RESPONSE_MODE
][0])
505 *url
= g_strconcat (*url
, "&response_mode=", g_uri_escape_string (OAUTH2info
[i
][OA2_RESPONSE_MODE
], NULL
, FALSE
), NULL
);
506 if(OAUTH2info
[i
][OA2_STATE
][0])
507 *url
= g_strconcat (*url
, "&state=", g_uri_escape_string (OAUTH2info
[i
][OA2_STATE
], NULL
, FALSE
), NULL
);
512 gint
oauth2_check_passwds (PrefsAccount
*ac_prefs
)
514 gchar
*uid
= g_strdup_printf("%d", ac_prefs
->account_id
);
516 OAUTH2Data
*OAUTH2Data
= g_malloc(sizeof(* OAUTH2Data
));
519 oauth2_init (OAUTH2Data
);
521 OAUTH2Data
->custom_client_id
= ac_prefs
->oauth2_client_id
;
522 OAUTH2Data
->custom_client_secret
= ac_prefs
->oauth2_client_secret
;
524 if(passwd_store_has_password(PWS_ACCOUNT
, uid
, PWS_ACCOUNT_OAUTH2_EXPIRY
)) {
525 expiry
= atoi(passwd_store_get_account(ac_prefs
->account_id
, PWS_ACCOUNT_OAUTH2_EXPIRY
));
526 if (expiry
> (g_get_real_time () / G_USEC_PER_SEC
)){
528 log_message(LOG_PROTOCOL
, _("OAuth2 access token still fresh\n"));
533 if(passwd_store_has_password(PWS_ACCOUNT
, uid
, PWS_ACCOUNT_OAUTH2_REFRESH
)) {
534 log_message(LOG_PROTOCOL
, _("OAuth2 obtaining access token using refresh token\n"));
535 OAUTH2Data
->refresh_token
= passwd_store_get_account(ac_prefs
->account_id
, PWS_ACCOUNT_OAUTH2_REFRESH
);
536 ret
= oauth2_use_refresh_token (ac_prefs
->oauth2_provider
, OAUTH2Data
);
537 }else if (passwd_store_has_password(PWS_ACCOUNT
, uid
, PWS_ACCOUNT_OAUTH2_AUTH
)) {
538 log_message(LOG_PROTOCOL
, _("OAuth2 trying for fresh access token with authorization code\n"));
539 ret
= oauth2_obtain_tokens (ac_prefs
->oauth2_provider
, OAUTH2Data
,
540 passwd_store_get_account(ac_prefs
->account_id
, PWS_ACCOUNT_OAUTH2_AUTH
));
546 log_message(LOG_PROTOCOL
, _("OAuth2 access token not obtained\n"));
548 passwd_store_set_account(ac_prefs
->account_id
, PWS_ACCOUNT_RECV
, OAUTH2Data
->access_token
, FALSE
);
549 if (ac_prefs
->use_smtp_auth
&& ac_prefs
->smtp_auth_type
== SMTPAUTH_OAUTH2
)
550 passwd_store_set_account(ac_prefs
->account_id
, PWS_ACCOUNT_SEND
, OAUTH2Data
->access_token
, FALSE
);
551 passwd_store_set_account(ac_prefs
->account_id
, PWS_ACCOUNT_OAUTH2_EXPIRY
, OAUTH2Data
->expiry_str
, FALSE
);
552 log_message(LOG_PROTOCOL
, _("OAuth2 access token updated\n"));
560 /* returns allocated string which must be freed */
561 guchar
* oauth2_decode(const gchar
*in
)
566 tmp
= g_base64_decode(in
, &len
);
567 passcrypt_decrypt(tmp
, len
);
572 void oauth2_encode(const gchar
*in
)
574 guchar
*tmp
= g_strdup(in
);
575 guchar
*tmp2
= g_strdup(in
);
577 gsize len
= strlen(in
);
579 passcrypt_encrypt(tmp
, len
);
580 result
= g_base64_encode(tmp
, len
);
581 tmp2
= oauth2_decode(result
);
583 log_message(LOG_PROTOCOL
, _("OAuth2 original: %s\n"), in
);
584 log_message(LOG_PROTOCOL
, _("OAuth2 encoded: %s\n"), result
);
585 log_message(LOG_PROTOCOL
, _("OAuth2 decoded: %s\n\n"), tmp2
);
592 gint
oauth2_init (OAUTH2Data
*OAUTH2Data
)
594 OAUTH2Data
->refresh_token
= NULL
;
595 OAUTH2Data
->access_token
= NULL
;
596 OAUTH2Data
->expiry_str
= NULL
;
597 OAUTH2Data
->expiry
= 0;
598 OAUTH2Data
->custom_client_id
= NULL
;
599 OAUTH2Data
->custom_client_secret
= NULL
;