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
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
)
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
)
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
))
78 /* code_challenge generator */
79 void oauth2_code_challenge(OAUTH2_S
*oauth2
)
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);
104 sprintf(cv
, "%s-%s", cv1
, cv2
);
105 fs_give((void **) &cv1
);
106 fs_give((void **) &cv2
);
107 oauth2
->param
[i
].value
= cv
;
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
);
117 for(; *v
!= '\0'; v
++){
118 if(*v
< 0x20) continue;
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
)
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)
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));
174 xoauth2_server(char *server
, char *tenant
)
179 if (server
== NULL
) return NULL
;
185 for(i
= 0; t
!= NULL
; i
++){
186 t
= strchr(t
, '\001');
189 rv
= fs_get((strlen(s
) + i
*(strlen(tenant
)-1) + 1)*sizeof(char));
191 for(u
= t
= s
; t
!= NULL
; i
++){
192 t
= strchr(t
, '\001');
193 if (t
!= NULL
) *t
= '\0';
210 oauth2_json_reply(OAUTH2_SERVER_METHOD_S RefreshMethod
, OAUTH2_S
*oauth2
, int *status
)
213 HTTP_PARAM_S params
[OAUTH2_PARAM_NUMBER
];
214 HTTPSTREAM
*stream
= NIL
;
218 LOAD_HTTP_PARAMS(RefreshMethod
, params
);
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
);
230 fs_give((void **) &server
);
237 mm_login_oauth2_c_client_method (NETMBX
*mb
, char *user
, char *method
,
238 OAUTH2_S
*oauth2
, unsigned long trial
, int *tryanother
)
244 if(oauth2
->param
[OA2_Id
].value
== NULL
245 || (oauth2
->require_secret
&& oauth2
->param
[OA2_Secret
].value
== NULL
)){
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
)){
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
);
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
)
279 case JString
: oauth2
->devicecode
.expires_in
= atoi((char *) jx
->value
);
281 case JLong
: oauth2
->devicecode
.expires_in
= *(long *) jx
->value
;
286 if((jx
= json_body_value(json
, "interval")) != NULL
)
288 case JString
: oauth2
->devicecode
.interval
= atoi((char *) jx
->value
);
290 case JLong
: oauth2
->devicecode
.interval
= *(long *) jx
->value
;
295 json_assign ((void **) &oauth2
->devicecode
.message
, json
, "message", JString
);
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
);
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
);
315 case HTTP_UNAUTHORIZED
:
316 mm_log("Client not authorized (wrong client-id?)", ERROR
);
317 oauth2
->cancel_refresh_token
++;
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
)
324 case JString
: oauth2
->expiration
= time(0) + atol((char *) jx
->value
);
326 case JLong
: oauth2
->expiration
= time(0) + *(long *) jx
->value
;
330 oauth2
->cancel_refresh_token
= 0; /* do not cancel this token. It is good */
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
);
341 if(err
) fs_give((void **) &err
);
342 if(err_desc
) fs_give((void **) &err_desc
);
343 oauth2
->cancel_refresh_token
++;
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
);
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
);
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
)
389 case JString
: oauth2
->expiration
= time(0) + atol((char *) jx
->value
);
391 case JLong
: oauth2
->expiration
= time(0) + *(long *) jx
->value
;
396 oauth2
->cancel_refresh_token
= 0; /* do not cancel this token. It is good */
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
);
409 if(err
) fs_give((void **) &err
);
410 if(err_desc
) fs_give((void **) &err_desc
);
411 oauth2
->cancel_refresh_token
++;
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
;
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
);
442 case HTTP_BAD
: json_assign ((void **) &error
, json
, "error", JString
);
444 if(compare_cstring(error
, "authorization_pending") == 0)
446 else if(compare_cstring(error
, "authorization_declined") == 0)
448 else if(compare_cstring(error
, "bad_verification_code") == 0)
450 else if(compare_cstring(error
, "expired_token") == 0)
452 else /* keep waiting? */
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
)
466 case JString
: oauth2
->expiration
= time(0) + atol((char *) jx
->value
);
468 case JLong
: oauth2
->expiration
= time(0) + *(long *) jx
->value
;
473 rv
= OA2_CODE_SUCCESS
;
474 oauth2
->cancel_refresh_token
= 0; /* do not cancel this token. It is good */
478 default : { char tmp
[100];
479 sprintf(tmp
, "Oauth device Received Code %d", status
);
481 oauth2
->cancel_refresh_token
++;
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
));
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
);
514 copy_xoauth2_info(XOAUTH2_INFO_S
*x
)
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
);
530 same_xoauth2_info(XOAUTH2_INFO_S x
, XOAUTH2_INFO_S y
)
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
))))
543 free_xoauth2_info_list(XOAUTH2_INFO_S
***xp
)
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
)
565 char user
[MAILTMPLEN
];
567 unsigned long trial
= 0;
569 memset((void *) &oauth2
, 0, sizeof(OAUTH2_S
));
570 mail_valid_net_parse(stream
->original_mailbox
, &mb
);
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
);
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
);