* Treat HTTP_BAD code as an error when trying to authenticate using XOAUTH2
[alpine.git] / imap / src / c-client / oauth2_aux.c
blobb396ccda341ec27b19fbf27819898646ad0e6700
1 /*
2 * ========================================================================
3 * Copyright 2013-2021 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 OA2_type i;
51 if(oauth2.param[OA2_Id].value) fs_give((void **) &oauth2.param[OA2_Id].value);
52 if(oauth2.param[OA2_Secret].value) fs_give((void **) &oauth2.param[OA2_Secret].value);
53 if(oauth2.param[OA2_Tenant].value) fs_give((void **) &oauth2.param[OA2_Tenant].value);
54 if(oauth2.param[OA2_State].value) fs_give((void **) &oauth2.param[OA2_State].value);
55 if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value);
56 if(oauth2.access_token) fs_give((void **) &oauth2.access_token);
58 /* free extra parameters generated by us */
59 OAUTH2_CLEAR_EXTRA(oauth2, "code_verifier");
60 OAUTH2_CLEAR_EXTRA(oauth2, "code_challenge");
61 OAUTH2_CLEAR_EXTRA(oauth2, "login_hint");
64 OA2_type oauth2_find_extra_parameter(OAUTH2_S *oauth2, char *name)
66 OA2_type i;
68 if(!name) return OA2_End;
70 for(i = OA2_Extra1; i < OA2_End; i++){
71 if(oauth2->param[i].name
72 && !compare_cstring(oauth2->param[i].name, name))
73 break;
75 return i;
78 /* code_challenge generator */
79 void oauth2_code_challenge(OAUTH2_S *oauth2)
81 OA2_type i, j, k;
82 char *cv, *cv1, *cv2;
84 i = oauth2_find_extra_parameter(oauth2, "code_verifier");
86 if(i == OA2_End) return;
88 j = oauth2_find_extra_parameter(oauth2, "code_challenge");
90 if(j == OA2_End) return;
92 k = oauth2_find_extra_parameter(oauth2, "code_challenge_method");
94 cv1 = oauth2_generate_state();
95 cv2 = oauth2_generate_state();
97 if(!cv1 || !cv2) return;
99 if(oauth2->param[i].value) fs_give((void **) &oauth2->param[i].value);
100 if(oauth2->param[j].value) fs_give((void **) &oauth2->param[j].value);
102 cv = fs_get(strlen(cv1) + strlen(cv2) + 1 + 1);
103 if(cv){
104 sprintf(cv, "%s-%s", cv1, cv2);
105 fs_give((void **) &cv1);
106 fs_give((void **) &cv2);
107 oauth2->param[i].value = cv;
108 if(k == OA2_End
109 || !compare_cstring(oauth2->param[k].value, "plain"))
110 oauth2->param[j].value = cpystr(cv);
111 else if(!compare_cstring(oauth2->param[k].value, "S256")){
112 unsigned char *t, *u, *v;
113 char *s = hash_from_sizedtext("SHA256", cv, strlen(cv), &t);
114 if(s){
115 u = v = t;
116 if(t){
117 for(; *v != '\0'; v++){
118 if(*v < 0x20) continue;
119 *u++ = *v;
121 *u = '\0';
122 oauth2->param[j].value = t;
124 fs_give((void **) &s);
130 /* code_challenge generator */
131 void oauth2_login_hint(OAUTH2_S *oauth2, char *user)
133 OA2_type i, j, k;
134 char *cv, *cv1, *cv2;
136 if(!user || !*user) return;
138 i = oauth2_find_extra_parameter(oauth2, "login_hint");
140 if(i == OA2_End) return;
142 if(oauth2->param[i].value) fs_give((void **) &oauth2->param[i].value);
143 oauth2->param[i].value = cpystr(user);
146 /* we generate something like a guid, but not care about
147 * anything, but that it is really random.
149 char *oauth2_generate_state(void)
151 char rv[37];
152 int i;
154 rv[0] = '\0';
155 for(i = 0; i < 4; i++)
156 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
157 sprintf(rv + strlen(rv), "%c", '-');
158 for(i = 0; i < 2; i++)
159 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
160 sprintf(rv + strlen(rv), "%c", '-');
161 for(i = 0; i < 2; i++)
162 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
163 sprintf(rv + strlen(rv), "%c", '-');
164 for(i = 0; i < 2; i++)
165 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
166 sprintf(rv + strlen(rv), "%c", '-');
167 for(i = 0; i < 6; i++)
168 sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256));
169 rv[36] = '\0';
170 return cpystr(rv);
173 char *
174 xoauth2_server(char *server, char *tenant)
176 char *rv = NULL;
177 char *s;
179 if (server == NULL) return NULL;
181 s = cpystr(server);
182 if(tenant){
183 char *t = s, *u;
184 int i;
185 for(i = 0; t != NULL; i++){
186 t = strchr(t, '\001');
187 if(t != NULL) t++;
189 rv = fs_get((strlen(s) + i*(strlen(tenant)-1) + 1)*sizeof(char));
190 *rv = '\0';
191 for(u = t = s; t != NULL; i++){
192 t = strchr(t, '\001');
193 if (t != NULL) *t = '\0';
194 strcat(rv, u);
195 if(t != NULL){
196 strcat(rv, tenant);
197 *t++ = '\001';
199 u = t;
203 else
204 rv = cpystr(server);
206 return rv;
209 JSON_S *
210 oauth2_json_reply(OAUTH2_SERVER_METHOD_S RefreshMethod, OAUTH2_S *oauth2, int *status)
212 JSON_S *json = NULL;
213 HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER];
214 HTTPSTREAM *stream = NIL;
215 unsigned char *s;
216 char *server = NULL;
218 LOAD_HTTP_PARAMS(RefreshMethod, params);
219 *status = 0;
220 server = xoauth2_server(RefreshMethod.urlserver, oauth2->param[OA2_Tenant].value);
221 if(strcmp(RefreshMethod.name, "POST") == 0
222 && ((stream = http_open(server)) != NULL)
223 && ((s = http_post_param(stream, params)) != NULL)){
224 json = json_parse(s);
225 fs_give((void **) &s);
227 *status = stream && stream->status ? stream->status->code : -1;
228 if(stream) http_close(stream);
229 if(server)
230 fs_give((void **) &server);
232 return json;
236 void
237 mm_login_oauth2_c_client_method (NETMBX *mb, char *user, char *method,
238 OAUTH2_S *oauth2, unsigned long trial, int *tryanother)
240 int status;
241 char *s = NULL;
242 JSON_S *json = NULL;
244 if(oauth2->param[OA2_Id].value == NULL
245 || (oauth2->require_secret && oauth2->param[OA2_Secret].value == NULL)){
246 XOAUTH2_INFO_S *x;
247 oauth2clientinfo_t ogci =
248 (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL);
250 if(ogci && (x = (*ogci)(oauth2->name, user)) != NULL){
251 oauth2->param[OA2_Id].value = cpystr(x->client_id);
252 oauth2->param[OA2_Secret].value = x->client_secret ? cpystr(x->client_secret) : NULL;
253 if(oauth2->param[OA2_Tenant].value) fs_give((void **) &oauth2->param[OA2_Tenant].value);
254 oauth2->param[OA2_Tenant].value = x->tenant ? cpystr(x->tenant) : NULL;
255 free_xoauth2_info(&x);
259 if(oauth2->param[OA2_Id].value == NULL
260 || (oauth2->require_secret && oauth2->param[OA2_Secret].value == NULL)){
261 *tryanother = 1;
262 return;
265 /* Do we have a method to execute? */
266 if (oauth2->first_time && oauth2->server_mthd[OA2_GetDeviceCode].name){
267 oauth2deviceinfo_t ogdi;
269 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetDeviceCode], oauth2, &status);
271 if(json != NULL){
272 JSON_S *jx;
274 json_assign ((void **) &oauth2->devicecode.device_code, json, "device_code", JString);
275 json_assign ((void **) &oauth2->devicecode.user_code, json, "user_code", JString);
276 json_assign ((void **) &oauth2->devicecode.verification_uri, json, "verification_uri", JString);
277 if((jx = json_body_value(json, "expires_in")) != NULL)
278 switch(jx->jtype){
279 case JString: oauth2->devicecode.expires_in = atoi((char *) jx->value);
280 break;
281 case JLong : oauth2->devicecode.expires_in = *(long *) jx->value;
282 break;
283 default : break;
286 if((jx = json_body_value(json, "interval")) != NULL)
287 switch(jx->jtype){
288 case JString: oauth2->devicecode.interval = atoi((char *) jx->value);
289 break;
290 case JLong : oauth2->devicecode.interval = *(long *) jx->value;
291 break;
292 default : break;
295 json_assign ((void **) &oauth2->devicecode.message, json, "message", JString);
296 json_free(&json);
298 if(oauth2->devicecode.verification_uri && oauth2->devicecode.user_code){
299 ogdi = (oauth2deviceinfo_t) mail_parameters (NIL, GET_OA2DEVICEINFO, NIL);
300 if(ogdi) (*ogdi)(oauth2, method);
303 return;
306 /* else check if we have a refresh token, and in that case use it */
308 if(oauth2->param[OA2_RefreshToken].value){
309 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken], oauth2, &status);
311 if(json != NULL){
312 JSON_S *jx;
314 switch(status){
315 case HTTP_UNAUTHORIZED:
316 mm_log("Client not authorized (wrong client-id?)", ERROR);
317 oauth2->cancel_refresh_token++;
318 break;
319 case HTTP_OK: if(oauth2->access_token)
320 fs_give((void **) &oauth2->access_token);
321 json_assign ((void **) &oauth2->access_token, json, "access_token", JString);
322 if((jx = json_body_value(json, "expires_in")) != NULL)
323 switch(jx->jtype){
324 case JString: oauth2->expiration = time(0) + atol((char *) jx->value);
325 break;
326 case JLong : oauth2->expiration = time(0) + *(long *) jx->value;
327 break;
328 default : break;
330 oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */
331 break;
333 default : { char tmp[200];
334 char *err, *err_desc;
335 jx = json_body_value(json, "error");
336 err = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "Unknown error");
337 jx = json_body_value(json, "error_description");
338 err_desc = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "No description");
339 sprintf(tmp, "Code %d: %.80s: %.80s", status, err, err_desc);
340 mm_log (tmp, ERROR);
341 if(err) fs_give((void **) &err);
342 if(err_desc) fs_give((void **) &err_desc);
343 oauth2->cancel_refresh_token++;
345 break;
347 json_free(&json);
349 return;
352 * else, we do not have a refresh token, nor an access token.
353 * We need to start the process to get an access code. We use this
354 * to get an access token and refresh token.
356 { OAUTH2_SERVER_METHOD_S RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode];
357 HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER];
359 LOAD_HTTP_PARAMS(RefreshMethod, params);
361 if(strcmp(RefreshMethod.name, "GET") == 0){
362 char *server = xoauth2_server(RefreshMethod.urlserver, oauth2->param[OA2_Tenant].value);
363 char *url = http_get_param_url(server, params);
364 oauth2getaccesscode_t ogac =
365 (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL);
367 if(ogac)
368 oauth2->param[OA2_Code].value = (*ogac)(url, method, oauth2, tryanother);
370 if(server) fs_give((void **) &server);
373 if(oauth2->param[OA2_Code].value){
374 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status);
376 if(json != NULL){
377 JSON_S *jx;
379 switch(status){
380 case HTTP_OK : if(oauth2->param[OA2_RefreshToken].value)
381 fs_give((void **) &oauth2->param[OA2_RefreshToken].value);
382 json_assign ((void **) &oauth2->param[OA2_RefreshToken].value, json, "refresh_token", JString);
383 if(oauth2->access_token)
384 fs_give((void **) &oauth2->access_token);
385 json_assign ((void **) &oauth2->access_token, json, "access_token", JString);
387 if((jx = json_body_value(json, "expires_in")) != NULL)
388 switch(jx->jtype){
389 case JString: oauth2->expiration = time(0) + atol((char *) jx->value);
390 break;
391 case JLong : oauth2->expiration = time(0) + *(long *) jx->value;
392 break;
393 default : break;
396 oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */
398 break;
400 case HTTP_BAD :
401 default : { char tmp[200];
402 char *err, *err_desc;
403 jx = json_body_value(json, "error");
404 err = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "Unknown error");
405 jx = json_body_value(json, "error_description");
406 err_desc = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "No description");
407 sprintf(tmp, "Code %d: %.80s: %.80s", status, err, err_desc);
408 mm_log (tmp, ERROR);
409 if(err) fs_give((void **) &err);
410 if(err_desc) fs_give((void **) &err_desc);
411 oauth2->cancel_refresh_token++;
415 json_free(&json);
418 return;
422 void
423 oauth2deviceinfo_get_accesscode(void *inp, void *outp)
425 OAUTH2_DEVICEPROC_S *oad = (OAUTH2_DEVICEPROC_S *) inp;
426 OAUTH2_S *oauth2 = oad->xoauth2;
427 OAUTH2_DEVICECODE_S *dcode = &oauth2->devicecode;
428 int done = 0, status, rv;
429 JSON_S *json;
431 if(dcode->device_code && oauth2->param[OA2_DeviceCode].value == NULL)
432 oauth2->param[OA2_DeviceCode].value = cpystr(dcode->device_code);
434 rv = OA2_CODE_WAIT; /* wait by default */
435 json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status);
437 if(json != NULL){
438 JSON_S *jx;
439 char *error = NIL;
441 switch(status){
442 case HTTP_BAD : json_assign ((void **) &error, json, "error", JString);
443 if(!error) break;
444 if(compare_cstring(error, "authorization_pending") == 0)
445 rv = OA2_CODE_WAIT;
446 else if(compare_cstring(error, "authorization_declined") == 0)
447 rv = OA2_CODE_FAIL;
448 else if(compare_cstring(error, "bad_verification_code") == 0)
449 rv = OA2_CODE_FAIL;
450 else if(compare_cstring(error, "expired_token") == 0)
451 rv = OA2_CODE_FAIL;
452 else /* keep waiting? */
453 rv = OA2_CODE_WAIT;
455 break;
457 case HTTP_OK : if(oauth2->param[OA2_RefreshToken].value)
458 fs_give((void **) &oauth2->param[OA2_RefreshToken].value);
459 json_assign ((void **) &oauth2->param[OA2_RefreshToken].value, json, "refresh_token", JString);
460 if(oauth2->access_token)
461 fs_give((void **) &oauth2->access_token);
462 json_assign ((void **) &oauth2->access_token, json, "access_token", JString);
464 if((jx = json_body_value(json, "expires_in")) != NULL)
465 switch(jx->jtype){
466 case JString: oauth2->expiration = time(0) + atol((char *) jx->value);
467 break;
468 case JLong : oauth2->expiration = time(0) + *(long *) jx->value;
469 break;
470 default : break;
473 rv = OA2_CODE_SUCCESS;
474 oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */
476 break;
478 default : { char tmp[100];
479 sprintf(tmp, "Oauth device Received Code %d", status);
480 mm_log (tmp, ERROR);
481 oauth2->cancel_refresh_token++;
485 json_free(&json);
488 *(int *)outp = rv;
491 XOAUTH2_INFO_S *
492 new_xoauth2_info(void)
494 XOAUTH2_INFO_S *rv = fs_get(sizeof(XOAUTH2_INFO_S));
495 memset((void *) rv, 0, sizeof(XOAUTH2_INFO_S));
496 return rv;
499 void
500 free_xoauth2_info(XOAUTH2_INFO_S **xp)
502 if(xp == NULL || *xp == NULL) return;
504 if((*xp)->name) fs_give((void **) &(*xp)->name);
505 if((*xp)->client_id) fs_give((void **) &(*xp)->client_id);
506 if((*xp)->client_secret) fs_give((void **) &(*xp)->client_secret);
507 if((*xp)->tenant) fs_give((void **) &(*xp)->tenant);
508 if((*xp)->flow) fs_give((void **) &(*xp)->flow);
509 if((*xp)->users) fs_give((void **) &(*xp)->users);
510 fs_give((void **) xp);
513 XOAUTH2_INFO_S *
514 copy_xoauth2_info(XOAUTH2_INFO_S *x)
516 XOAUTH2_INFO_S *y;
518 if(x == NULL) return NULL;
519 y = new_xoauth2_info();
520 if(x->name) y->name = cpystr(x->name);
521 if(x->client_id) y->client_id = cpystr(x->client_id);
522 if(x->client_secret) y->client_secret = cpystr(x->client_secret);
523 if(x->tenant) y->tenant = cpystr(x->tenant);
524 if(x->flow) y->flow = cpystr(x->flow);
525 if(x->users) y->users = cpystr(x->users);
526 return y;
530 same_xoauth2_info(XOAUTH2_INFO_S x, XOAUTH2_INFO_S y)
532 int rv = 0;
533 if(x.name && y.name && !strcmp((char *) x.name, (char *) y.name)
534 && x.client_id && y.client_id && !strcmp(x.client_id, y.client_id)
535 && ((!x.client_secret && !y.client_secret)
536 || (x.client_secret && y.client_secret && !strcmp(x.client_secret, y.client_secret)))
537 && ((!x.tenant && !y.tenant) || (x.tenant && y.tenant && !strcmp(x.tenant, y.tenant))))
538 rv = 1;
539 return rv;
542 void
543 free_xoauth2_info_list(XOAUTH2_INFO_S ***xp)
545 int i;
547 if(xp == NULL || *xp == NULL || **xp) return;
548 for(i = 0; (*xp)[i] != NULL; i++) free_xoauth2_info(&(*xp)[i]);
549 fs_give((void **) &xp);
553 /* This function does not create a refresh token and and
554 * an access token, but uses an already known refresh token
555 * to generate a refresh token on an ALREADY OPEN stream.
556 * The assumption is that the user has already unlocked all
557 * passwords and the app can access them from some source
558 * (key chain/credentials/memory) to go through this
559 * process seamlessly.
561 void renew_accesstoken(MAILSTREAM *stream)
563 OAUTH2_S oauth2;
564 NETMBX mb;
565 char user[MAILTMPLEN];
566 int tryanother;
567 unsigned long trial = 0;
569 memset((void *) &oauth2, 0, sizeof(OAUTH2_S));
570 mail_valid_net_parse(stream->original_mailbox, &mb);
571 user[0] = '\0';
572 mm_login_method (&mb, user, (void *) &oauth2, trial, stream->auth.name);
574 if(oauth2.access_token) /* we need a new one */
575 fs_give((void **) &oauth2.access_token);
577 if(stream->auth.expiration == 0){
578 stream->auth.expiration = oauth2.expiration;
579 if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value);
580 return;
583 oauth2.param[OA2_State].value = oauth2_generate_state();
585 mm_login_oauth2_c_client_method (&mb, user, stream->auth.name, &oauth2, trial, &tryanother);
587 if(oauth2.access_token)
588 mm_login_method (&mb, user, (void *) &oauth2, trial, stream->auth.name);
590 stream->auth.expiration = oauth2.expiration;
591 oauth2_free_extra_values(oauth2);