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
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 */
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) { \
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; \
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); \
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
)
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
))
76 /* code_challenge generator */
77 void oauth2_code_challenge(OAUTH2_S
*oauth2
)
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);
102 sprintf(cv
, "%s-%s", cv1
, cv2
);
103 fs_give((void **) &cv1
);
104 fs_give((void **) &cv2
);
105 oauth2
->param
[i
].value
= cv
;
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
);
115 for(; *v
!= '\0'; v
++){
116 if(*v
< 0x20) continue;
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
)
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)
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));
172 xoauth2_server(char *server
, char *tenant
)
176 if (server
== NULL
) return NULL
;
179 char *s
= cpystr(server
);
182 for(i
= 0; t
!= NULL
; i
++){
183 t
= strchr(t
, '\001');
186 rv
= fs_get((strlen(s
) + i
*(strlen(tenant
)-1) + 1)*sizeof(char));
188 for(u
= t
= s
; t
!= NULL
; i
++){
189 t
= strchr(t
, '\001');
190 if (t
!= NULL
) *t
= '\0';
198 fs_give((void **) &s
);
207 oauth2_json_reply(OAUTH2_SERVER_METHOD_S RefreshMethod
, OAUTH2_S
*oauth2
, int *status
)
210 HTTP_PARAM_S params
[OAUTH2_PARAM_NUMBER
];
211 HTTPSTREAM
*stream
= NIL
;
215 LOAD_HTTP_PARAMS(RefreshMethod
, params
);
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
);
227 fs_give((void **) &server
);
234 mm_login_oauth2_c_client_method (NETMBX
*mb
, char *user
, char *method
,
235 OAUTH2_S
*oauth2
, unsigned long trial
, int *tryanother
)
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
)){
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
)){
263 oauth2_free_extra_values(*oauth2
);
264 mm_log("could not get client-id or client-secret required and empty.", (long) NIL
);
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
);
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
)
285 case JString
: oauth2
->devicecode
.expires_in
= atoi((char *) jx
->value
);
287 case JLong
: oauth2
->devicecode
.expires_in
= *(long *) jx
->value
;
292 if((jx
= json_body_value(json
, "interval")) != NULL
)
294 case JString
: oauth2
->devicecode
.interval
= atoi((char *) jx
->value
);
296 case JLong
: oauth2
->devicecode
.interval
= *(long *) jx
->value
;
301 json_assign ((void **) &oauth2
->devicecode
.message
, json
, "message", JString
);
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
);
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
);
325 case HTTP_UNAUTHORIZED
:
326 mm_log("Client not authorized (wrong client-id?)", ERROR
);
327 oauth2
->cancel_refresh_token
++;
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
)
334 case JString
: oauth2
->expiration
= time(0) + atol((char *) jx
->value
);
336 case JLong
: oauth2
->expiration
= time(0) + *(long *) jx
->value
;
340 oauth2
->cancel_refresh_token
= 0; /* do not cancel this token. It is good */
341 mm_log("Got new refresh token.", (long) NIL
);
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
);
352 if(err
) fs_give((void **) &err
);
353 if(err_desc
) fs_give((void **) &err_desc
);
354 oauth2
->cancel_refresh_token
++;
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
);
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
);
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
)
403 case JString
: oauth2
->expiration
= time(0) + atol((char *) jx
->value
);
405 case JLong
: oauth2
->expiration
= time(0) + *(long *) jx
->value
;
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
);
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
);
422 if(err
) fs_give((void **) &err
);
423 if(err_desc
) fs_give((void **) &err_desc
);
424 oauth2
->cancel_refresh_token
++;
431 mm_log("Failed to obtain authorization code. Cancelled by user?", (long) NIL
);
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
;
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
);
458 case HTTP_BAD
: json_assign ((void **) &error
, json
, "error", JString
);
460 if(compare_cstring(error
, "authorization_pending") == 0)
462 else if(compare_cstring(error
, "authorization_declined") == 0)
464 else if(compare_cstring(error
, "bad_verification_code") == 0)
466 else if(compare_cstring(error
, "expired_token") == 0)
468 else /* keep waiting? */
470 mm_log(rv
== OA2_CODE_FAIL
? error
: "waiting for process to end.",
471 rv
== OA2_CODE_FAIL
? ERROR
: (long) NIL
);
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
)
483 case JString
: oauth2
->expiration
= time(0) + atol((char *) jx
->value
);
485 case JLong
: oauth2
->expiration
= time(0) + *(long *) jx
->value
;
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
);
495 default : { char tmp
[100];
496 sprintf(tmp
, "Oauth device Received Code %d.", status
);
498 oauth2
->cancel_refresh_token
++;
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
));
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
);
530 copy_xoauth2_info(XOAUTH2_INFO_S
*x
)
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
);
546 same_xoauth2_info(XOAUTH2_INFO_S x
, XOAUTH2_INFO_S y
)
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
))))
559 free_xoauth2_info_list(XOAUTH2_INFO_S
***xp
)
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
)
581 char user
[MAILTMPLEN
];
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
);
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
);
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
);