* Implement a different way to delete a password from the cache.
[alpine.git] / imap / src / c-client / oauth2_aux.c
blob26730de78cb653f56826dd11a200d2f8f18e1a71
1 /*
2 * ========================================================================
3 * Copyright 2013-2022 Eduardo Chappa
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * ========================================================================
15 /* OAUTH2 support code goes here. This is necessary because
16 * 1. it helps to coordinate two different methods, such as XOAUTH2 and
17 * OAUTHBEARER, which use the same code, so it can all go in one place
19 * 2. It helps with coordinating with the client when the server requires
20 * the deviceinfo method.
23 /* http.h is supposed to be included, typically by including c-client.h */
24 #include "json.h"
25 #include "oauth2_aux.h"
27 OA2_type oauth2_find_extra_parameter(OAUTH2_S *, char *);
28 JSON_S *oauth2_json_reply(OAUTH2_SERVER_METHOD_S, OAUTH2_S *, int *);
29 char *xoauth2_server(char *, char *);
31 #define LOAD_HTTP_PARAMS(X, Y) { \
32 int i; \
33 for(i = 0; (X).params[i] != OA2_End; i++){ \
34 OA2_type j = (X).params[i]; \
35 (Y)[i].name = oauth2->param[j].name; \
36 (Y)[i].value = oauth2->param[j].value; \
37 } \
38 (Y)[i].name = (Y)[i].value = NULL; \
41 #define OAUTH2_CLEAR_EXTRA(X, Y) do { \
42 OA2_type i = oauth2_find_extra_parameter(&(X), (Y)); \
43 if(i < OA2_End && (X).param[i].value) \
44 fs_give((void **) &(X).param[i].value); \
45 } while(0)
47 void oauth2_free_extra_values(OAUTH2_S oauth2)
49 if(oauth2.param[OA2_Id].value) fs_give((void **) &oauth2.param[OA2_Id].value);
50 if(oauth2.param[OA2_Secret].value) fs_give((void **) &oauth2.param[OA2_Secret].value);
51 if(oauth2.param[OA2_Tenant].value) fs_give((void **) &oauth2.param[OA2_Tenant].value);
52 if(oauth2.param[OA2_State].value) fs_give((void **) &oauth2.param[OA2_State].value);
53 if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value);
54 if(oauth2.access_token) fs_give((void **) &oauth2.access_token);
56 /* free extra parameters generated by us */
57 OAUTH2_CLEAR_EXTRA(oauth2, "code_verifier");
58 OAUTH2_CLEAR_EXTRA(oauth2, "code_challenge");
59 OAUTH2_CLEAR_EXTRA(oauth2, "login_hint");
62 OA2_type oauth2_find_extra_parameter(OAUTH2_S *oauth2, char *name)
64 OA2_type i;
66 if(!name) return OA2_End;
68 for(i = OA2_Extra1; i < OA2_End; i++){
69 if(oauth2->param[i].name
70 && !compare_cstring(oauth2->param[i].name, name))
71 break;
73 return i;
76 /* code_challenge generator */
77 void oauth2_code_challenge(OAUTH2_S *oauth2)
79 OA2_type i, j, k;
80 char *cv, *cv1, *cv2;
82 i = oauth2_find_extra_parameter(oauth2, "code_verifier");
84 if(i == OA2_End) return;
86 j = oauth2_find_extra_parameter(oauth2, "code_challenge");
88 if(j == OA2_End) return;
90 k = oauth2_find_extra_parameter(oauth2, "code_challenge_method");
92 cv1 = oauth2_generate_state();
93 cv2 = oauth2_generate_state();
95 if(!cv1 || !cv2) return;
97 if(oauth2->param[i].value) fs_give((void **) &oauth2->param[i].value);
98 if(oauth2->param[j].value) fs_give((void **) &oauth2->param[j].value);
100 cv = fs_get(strlen(cv1) + strlen(cv2) + 1 + 1);
101 if(cv){
102 sprintf(cv, "%s-%s", cv1, cv2);
103 fs_give((void **) &cv1);
104 fs_give((void **) &cv2);
105 oauth2->param[i].value = cv;
106 if(k == OA2_End
107 || !compare_cstring(oauth2->param[k].value, "plain"))
108 oauth2->param[j].value = cpystr(cv);
109 else if(!compare_cstring(oauth2->param[k].value, "S256")){
110 unsigned char *t, *u, *v;
111 char *s = hash_from_sizedtext("SHA256", cv, strlen(cv), &t);
112 if(s){
113 u = v = t;
114 if(t){
115 for(; *v != '\0'; v++){
116 if(*v < 0x20) continue;
117 *u++ = *v;
119 *u = '\0';
120 oauth2->param[j].value = t;
122 fs_give((void **) &s);
128 /* code_challenge generator */
129 void oauth2_login_hint(OAUTH2_S *oauth2, char *user)
131 OA2_type i, j, k;
132 char *cv, *cv1, *cv2;
134 if(!user || !*user) return;
136 i = oauth2_find_extra_parameter(oauth2, "login_hint");
138 if(i == OA2_End) return;
140 if(oauth2->param[i].value) fs_give((void **) &oauth2->param[i].value);
141 oauth2->param[i].value = cpystr(user);
144 /* we generate something like a guid, but not care about
145 * anything, but that it is really random.
147 char *oauth2_generate_state(void)
149 char rv[37];
150 int i;
152 rv[0] = '\0';
153 for(i = 0; i < 4; i++)
154 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
155 sprintf(rv + strlen(rv), "%c", '-');
156 for(i = 0; i < 2; i++)
157 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
158 sprintf(rv + strlen(rv), "%c", '-');
159 for(i = 0; i < 2; i++)
160 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
161 sprintf(rv + strlen(rv), "%c", '-');
162 for(i = 0; i < 2; i++)
163 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
164 sprintf(rv + strlen(rv), "%c", '-');
165 for(i = 0; i < 6; i++)
166 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
167 rv[36] = '\0';
168 return cpystr(rv);
171 char *
172 xoauth2_server(char *server, char *tenant)
174 char *rv = NULL;
176 if (server == NULL) return NULL;
178 if(tenant){
179 char *s = cpystr(server);
180 char *t = s, *u;
181 int i;
182 for(i = 0; t != NULL; i++){
183 t = strchr(t, '\001');
184 if(t != NULL) t++;
186 rv = fs_get((strlen(s) + i*(strlen(tenant)-1) + 1)*sizeof(char));
187 *rv = '\0';
188 for(u = t = s; t != NULL; i++){
189 t = strchr(t, '\001');
190 if (t != NULL) *t = '\0';
191 strcat(rv, u);
192 if(t != NULL){
193 strcat(rv, tenant);
194 *t++ = '\001';
196 u = t;
198 fs_give((void **) &s);
200 else
201 rv = cpystr(server);
203 return rv;
206 JSON_S *
207 oauth2_json_reply(OAUTH2_SERVER_METHOD_S RefreshMethod, OAUTH2_S *oauth2, int *status)
209 JSON_S *json = NULL;
210 HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER];
211 HTTPSTREAM *stream = NIL;
212 unsigned char *s;
213 char *server = NULL;
215 LOAD_HTTP_PARAMS(RefreshMethod, params);
216 *status = 0;
217 server = xoauth2_server(RefreshMethod.urlserver, oauth2->param[OA2_Tenant].value);
218 if(strcmp(RefreshMethod.name, "POST") == 0
219 && ((stream = http_open(server)) != NULL)
220 && ((s = http_post_param(stream, params)) != NULL)){
221 json = json_parse(s);
222 fs_give((void **) &s);
224 *status = stream && stream->status ? stream->status->code : -1;
225 if(stream) http_close(stream);
226 if(server)
227 fs_give((void **) &server);
229 return json;
233 void
234 mm_login_oauth2_c_client_method (NETMBX *mb, char *user, char *method,
235 OAUTH2_S *oauth2, unsigned long trial, int *tryanother)
237 int status;
238 char *s = NULL;
239 JSON_S *json = NULL;
241 mm_log("mm_login_oauth2_c_client_method()", (long) NIL);
243 if(oauth2->param[OA2_Id].value == NULL
244 || (oauth2->require_secret && oauth2->param[OA2_Secret].value == NULL)){
245 XOAUTH2_INFO_S *x;
246 oauth2clientinfo_t ogci =
247 (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL);
249 mm_log("Setting up for next call. Attempting to ask client for client-id and client-secret.", (long) NIL);
251 if(ogci && (x = (*ogci)(oauth2->name, user)) != NULL){
252 oauth2->param[OA2_Id].value = cpystr(x->client_id);
253 oauth2->param[OA2_Secret].value = x->client_secret ? cpystr(x->client_secret) : NULL;
254 if(oauth2->param[OA2_Tenant].value) fs_give((void **) &oauth2->param[OA2_Tenant].value);
255 oauth2->param[OA2_Tenant].value = x->tenant ? cpystr(x->tenant) : NULL;
256 free_xoauth2_info(&x);
260 if(oauth2->param[OA2_Id].value == NULL
261 || (oauth2->require_secret && oauth2->param[OA2_Secret].value == NULL)){
262 *tryanother = 1;
263 oauth2_free_extra_values(*oauth2);
264 mm_log("could not get client-id or client-secret required and empty.", (long) NIL);
265 return;
268 mm_log("Got a client-id/client-secret to use.", (long) NIL);
270 /* Do we have a method to execute? */
271 if (oauth2->first_time && oauth2->server_mthd[OA2_GetDeviceCode].name){
272 oauth2deviceinfo_t ogdi;
274 mm_log("Attempting DEVICE method.", (long) NIL);
275 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetDeviceCode], oauth2, &status);
277 if(json != NULL){
278 JSON_S *jx;
280 json_assign ((void **) &oauth2->devicecode.device_code, json, "device_code", JString);
281 json_assign ((void **) &oauth2->devicecode.user_code, json, "user_code", JString);
282 json_assign ((void **) &oauth2->devicecode.verification_uri, json, "verification_uri", JString);
283 if((jx = json_body_value(json, "expires_in")) != NULL)
284 switch(jx->jtype){
285 case JString: oauth2->devicecode.expires_in = atoi((char *) jx->value);
286 break;
287 case JLong : oauth2->devicecode.expires_in = *(long *) jx->value;
288 break;
289 default : break;
292 if((jx = json_body_value(json, "interval")) != NULL)
293 switch(jx->jtype){
294 case JString: oauth2->devicecode.interval = atoi((char *) jx->value);
295 break;
296 case JLong : oauth2->devicecode.interval = *(long *) jx->value;
297 break;
298 default : break;
301 json_assign ((void **) &oauth2->devicecode.message, json, "message", JString);
302 json_free(&json);
304 if(oauth2->devicecode.verification_uri && oauth2->devicecode.user_code){
305 ogdi = (oauth2deviceinfo_t) mail_parameters (NIL, GET_OA2DEVICEINFO, NIL);
306 if(ogdi) (*ogdi)(oauth2, method, mb);
308 mm_log("Got Json reply. Completed parsing.", (long) NIL);
310 return;
313 /* else check if we have a refresh token, and in that case use it */
315 if(oauth2->param[OA2_RefreshToken].value){
317 mm_log("Attempting to get access token with known refresh token.", (long) NIL);
319 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken], oauth2, &status);
321 if(json != NULL){
322 JSON_S *jx;
324 switch(status){
325 case HTTP_UNAUTHORIZED:
326 mm_log("Client not authorized (wrong client-id?)", ERROR);
327 oauth2->cancel_refresh_token++;
328 break;
329 case HTTP_OK: if(oauth2->access_token)
330 fs_give((void **) &oauth2->access_token);
331 json_assign ((void **) &oauth2->access_token, json, "access_token", JString);
332 if((jx = json_body_value(json, "expires_in")) != NULL)
333 switch(jx->jtype){
334 case JString: oauth2->expiration = time(0) + atol((char *) jx->value);
335 break;
336 case JLong : oauth2->expiration = time(0) + *(long *) jx->value;
337 break;
338 default : break;
340 oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */
341 mm_log("Got new refresh token.", (long) NIL);
342 break;
344 default : { char tmp[200];
345 char *err, *err_desc;
346 jx = json_body_value(json, "error");
347 err = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "Unknown error");
348 jx = json_body_value(json, "error_description");
349 err_desc = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "No description");
350 sprintf(tmp, "Code %d: %.80s: %.80s", status, err, err_desc);
351 mm_log (tmp, ERROR);
352 if(err) fs_give((void **) &err);
353 if(err_desc) fs_give((void **) &err_desc);
354 oauth2->cancel_refresh_token++;
356 break;
358 json_free(&json);
360 return;
363 * else, we do not have a refresh token, nor an access token.
364 * We need to start the process to get an access code. We use this
365 * to get an access token and refresh token.
367 { OAUTH2_SERVER_METHOD_S RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode];
368 HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER];
370 mm_log("Starting AUTHORIZE method. No refresh token nor access token found.", (long) NIL);
372 LOAD_HTTP_PARAMS(RefreshMethod, params);
374 if(strcmp(RefreshMethod.name, "GET") == 0){
375 char *server = xoauth2_server(RefreshMethod.urlserver, oauth2->param[OA2_Tenant].value);
376 char *url = http_get_param_url(server, params);
377 oauth2getaccesscode_t ogac =
378 (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL);
380 if(ogac)
381 oauth2->param[OA2_Code].value = (*ogac)(url, method, oauth2, mb, tryanother);
383 if(server) fs_give((void **) &server);
384 if(url) fs_give((void **) &url);
387 if(oauth2->param[OA2_Code].value){
388 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status);
390 if(json != NULL){
391 JSON_S *jx;
393 switch(status){
394 case HTTP_OK : if(oauth2->param[OA2_RefreshToken].value)
395 fs_give((void **) &oauth2->param[OA2_RefreshToken].value);
396 json_assign ((void **) &oauth2->param[OA2_RefreshToken].value, json, "refresh_token", JString);
397 if(oauth2->access_token)
398 fs_give((void **) &oauth2->access_token);
399 json_assign ((void **) &oauth2->access_token, json, "access_token", JString);
401 if((jx = json_body_value(json, "expires_in")) != NULL)
402 switch(jx->jtype){
403 case JString: oauth2->expiration = time(0) + atol((char *) jx->value);
404 break;
405 case JLong : oauth2->expiration = time(0) + *(long *) jx->value;
406 break;
407 default : break;
409 oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */
410 mm_log("Got new refresh and access token.", (long) NIL);
411 break;
413 case HTTP_BAD :
414 default : { char tmp[200];
415 char *err, *err_desc;
416 jx = json_body_value(json, "error");
417 err = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "Unknown error");
418 jx = json_body_value(json, "error_description");
419 err_desc = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "No description");
420 sprintf(tmp, "Code %d: %.80s: %.80s", status, err, err_desc);
421 mm_log (tmp, ERROR);
422 if(err) fs_give((void **) &err);
423 if(err_desc) fs_give((void **) &err_desc);
424 oauth2->cancel_refresh_token++;
427 json_free(&json);
430 else
431 mm_log("Failed to obtain authorization code. Cancelled by user?", (long) NIL);
432 return;
436 void
437 oauth2deviceinfo_get_accesscode(void *inp, void *outp)
439 OAUTH2_DEVICEPROC_S *oad = (OAUTH2_DEVICEPROC_S *) inp;
440 OAUTH2_S *oauth2 = oad->xoauth2;
441 OAUTH2_DEVICECODE_S *dcode = &oauth2->devicecode;
442 int done = 0, status, rv;
443 JSON_S *json;
445 mm_log("oauth2deviceinfo: getting accesscode.", (long) NIL);
447 if(dcode->device_code && oauth2->param[OA2_DeviceCode].value == NULL)
448 oauth2->param[OA2_DeviceCode].value = cpystr(dcode->device_code);
450 rv = OA2_CODE_WAIT; /* wait by default */
451 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status);
453 if(json != NULL){
454 JSON_S *jx;
455 char *error = NIL;
457 switch(status){
458 case HTTP_BAD : json_assign ((void **) &error, json, "error", JString);
459 if(!error) break;
460 if(compare_cstring(error, "authorization_pending") == 0)
461 rv = OA2_CODE_WAIT;
462 else if(compare_cstring(error, "authorization_declined") == 0)
463 rv = OA2_CODE_FAIL;
464 else if(compare_cstring(error, "bad_verification_code") == 0)
465 rv = OA2_CODE_FAIL;
466 else if(compare_cstring(error, "expired_token") == 0)
467 rv = OA2_CODE_FAIL;
468 else /* keep waiting? */
469 rv = OA2_CODE_WAIT;
470 mm_log(rv == OA2_CODE_FAIL ? error : "waiting for process to end.",
471 rv == OA2_CODE_FAIL ? ERROR : (long) NIL);
472 break;
474 case HTTP_OK : if(oauth2->param[OA2_RefreshToken].value)
475 fs_give((void **) &oauth2->param[OA2_RefreshToken].value);
476 json_assign ((void **) &oauth2->param[OA2_RefreshToken].value, json, "refresh_token", JString);
477 if(oauth2->access_token)
478 fs_give((void **) &oauth2->access_token);
479 json_assign ((void **) &oauth2->access_token, json, "access_token", JString);
481 if((jx = json_body_value(json, "expires_in")) != NULL)
482 switch(jx->jtype){
483 case JString: oauth2->expiration = time(0) + atol((char *) jx->value);
484 break;
485 case JLong : oauth2->expiration = time(0) + *(long *) jx->value;
486 break;
487 default : break;
490 rv = OA2_CODE_SUCCESS;
491 oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */
492 mm_log("Got new refresh and access token.", (long) NIL);
493 break;
495 default : { char tmp[100];
496 sprintf(tmp, "Oauth device Received Code %d.", status);
497 mm_log (tmp, ERROR);
498 oauth2->cancel_refresh_token++;
501 json_free(&json);
504 *(int *)outp = rv;
507 XOAUTH2_INFO_S *
508 new_xoauth2_info(void)
510 XOAUTH2_INFO_S *rv = fs_get(sizeof(XOAUTH2_INFO_S));
511 memset((void *) rv, 0, sizeof(XOAUTH2_INFO_S));
512 return rv;
515 void
516 free_xoauth2_info(XOAUTH2_INFO_S **xp)
518 if(xp == NULL || *xp == NULL) return;
520 if((*xp)->name) fs_give((void **) &(*xp)->name);
521 if((*xp)->client_id) fs_give((void **) &(*xp)->client_id);
522 if((*xp)->client_secret) fs_give((void **) &(*xp)->client_secret);
523 if((*xp)->tenant) fs_give((void **) &(*xp)->tenant);
524 if((*xp)->flow) fs_give((void **) &(*xp)->flow);
525 if((*xp)->users) fs_give((void **) &(*xp)->users);
526 fs_give((void **) xp);
529 XOAUTH2_INFO_S *
530 copy_xoauth2_info(XOAUTH2_INFO_S *x)
532 XOAUTH2_INFO_S *y;
534 if(x == NULL) return NULL;
535 y = new_xoauth2_info();
536 if(x->name) y->name = cpystr(x->name);
537 if(x->client_id) y->client_id = cpystr(x->client_id);
538 if(x->client_secret) y->client_secret = cpystr(x->client_secret);
539 if(x->tenant) y->tenant = cpystr(x->tenant);
540 if(x->flow) y->flow = cpystr(x->flow);
541 if(x->users) y->users = cpystr(x->users);
542 return y;
546 same_xoauth2_info(XOAUTH2_INFO_S x, XOAUTH2_INFO_S y)
548 int rv = 0;
549 if(x.name && y.name && !strcmp((char *) x.name, (char *) y.name)
550 && x.client_id && y.client_id && !strcmp(x.client_id, y.client_id)
551 && ((!x.client_secret && !y.client_secret)
552 || (x.client_secret && y.client_secret && !strcmp(x.client_secret, y.client_secret)))
553 && ((!x.tenant && !y.tenant) || (x.tenant && y.tenant && !strcmp(x.tenant, y.tenant))))
554 rv = 1;
555 return rv;
558 void
559 free_xoauth2_info_list(XOAUTH2_INFO_S ***xp)
561 int i;
563 if(xp == NULL || *xp == NULL || **xp == NULL) return;
564 for(i = 0; (*xp)[i] != NULL; i++) free_xoauth2_info(&(*xp)[i]);
565 fs_give((void **) xp);
569 /* This function does not create a refresh token and and
570 * an access token, but uses an already known refresh token
571 * to generate a refresh token on an ALREADY OPEN stream.
572 * The assumption is that the user has already unlocked all
573 * passwords and the app can access them from some source
574 * (key chain/credentials/memory) to go through this
575 * process seamlessly.
577 void renew_accesstoken(MAILSTREAM *stream)
579 OAUTH2_S oauth2;
580 NETMBX mb;
581 char user[MAILTMPLEN];
582 int tryanother;
583 unsigned long trial = 0;
585 mm_log("renew_accesstoken().", (long) NIL);
587 memset((void *) &oauth2, 0, sizeof(OAUTH2_S));
588 mail_valid_net_parse(stream->original_mailbox, &mb);
589 user[0] = '\0';
590 mm_login_method (&mb, user, (void *) &oauth2, trial, stream->auth.name);
592 if(oauth2.access_token) /* we need a new one */
593 fs_give((void **) &oauth2.access_token);
595 if(stream->auth.expiration == 0){
596 stream->auth.expiration = oauth2.expiration;
597 if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value);
598 return;
601 oauth2.param[OA2_State].value = oauth2_generate_state();
603 mm_login_oauth2_c_client_method (&mb, user, stream->auth.name, &oauth2, trial, &tryanother);
605 if(oauth2.access_token)
606 mm_login_method (&mb, user, (void *) &oauth2, trial, stream->auth.name);
608 stream->auth.expiration = oauth2.expiration;
609 oauth2_free_extra_values(oauth2);