/* * ======================================================================== * Copyright 2013-2022 Eduardo Chappa * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * ======================================================================== */ /* OAUTH2 support code goes here. This is necessary because * 1. it helps to coordinate two different methods, such as XOAUTH2 and * OAUTHBEARER, which use the same code, so it can all go in one place * * 2. It helps with coordinating with the client when the server requires * the deviceinfo method. */ /* http.h is supposed to be included, typically by including c-client.h */ #include "json.h" #include "oauth2_aux.h" OA2_type oauth2_find_extra_parameter(OAUTH2_S *, char *); JSON_S *oauth2_json_reply(OAUTH2_SERVER_METHOD_S, OAUTH2_S *, int *); char *xoauth2_server(char *, char *); #define LOAD_HTTP_PARAMS(X, Y) { \ int i; \ for(i = 0; (X).params[i] != OA2_End; i++){ \ OA2_type j = (X).params[i]; \ (Y)[i].name = oauth2->param[j].name; \ (Y)[i].value = oauth2->param[j].value; \ } \ (Y)[i].name = (Y)[i].value = NULL; \ } #define OAUTH2_CLEAR_EXTRA(X, Y) do { \ OA2_type i = oauth2_find_extra_parameter(&(X), (Y)); \ if(i < OA2_End && (X).param[i].value) \ fs_give((void **) &(X).param[i].value); \ } while(0) void oauth2_free_extra_values(OAUTH2_S oauth2) { if(oauth2.param[OA2_Id].value) fs_give((void **) &oauth2.param[OA2_Id].value); if(oauth2.param[OA2_Secret].value) fs_give((void **) &oauth2.param[OA2_Secret].value); if(oauth2.param[OA2_Tenant].value) fs_give((void **) &oauth2.param[OA2_Tenant].value); if(oauth2.param[OA2_State].value) fs_give((void **) &oauth2.param[OA2_State].value); if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value); if(oauth2.access_token) fs_give((void **) &oauth2.access_token); /* free extra parameters generated by us */ OAUTH2_CLEAR_EXTRA(oauth2, "code_verifier"); OAUTH2_CLEAR_EXTRA(oauth2, "code_challenge"); OAUTH2_CLEAR_EXTRA(oauth2, "login_hint"); } OA2_type oauth2_find_extra_parameter(OAUTH2_S *oauth2, char *name) { OA2_type i; if(!name) return OA2_End; for(i = OA2_Extra1; i < OA2_End; i++){ if(oauth2->param[i].name && !compare_cstring(oauth2->param[i].name, name)) break; } return i; } /* code_challenge generator */ void oauth2_code_challenge(OAUTH2_S *oauth2) { OA2_type i, j, k; char *cv, *cv1, *cv2; i = oauth2_find_extra_parameter(oauth2, "code_verifier"); if(i == OA2_End) return; j = oauth2_find_extra_parameter(oauth2, "code_challenge"); if(j == OA2_End) return; k = oauth2_find_extra_parameter(oauth2, "code_challenge_method"); cv1 = oauth2_generate_state(); cv2 = oauth2_generate_state(); if(!cv1 || !cv2) return; if(oauth2->param[i].value) fs_give((void **) &oauth2->param[i].value); if(oauth2->param[j].value) fs_give((void **) &oauth2->param[j].value); cv = fs_get(strlen(cv1) + strlen(cv2) + 1 + 1); if(cv){ sprintf(cv, "%s-%s", cv1, cv2); fs_give((void **) &cv1); fs_give((void **) &cv2); oauth2->param[i].value = cv; if(k == OA2_End || !compare_cstring(oauth2->param[k].value, "plain")) oauth2->param[j].value = cpystr(cv); else if(!compare_cstring(oauth2->param[k].value, "S256")){ unsigned char *t, *u, *v; char *s = hash_from_sizedtext("SHA256", cv, strlen(cv), &t); if(s){ u = v = t; if(t){ for(; *v != '\0'; v++){ if(*v < 0x20) continue; *u++ = *v; } *u = '\0'; oauth2->param[j].value = t; } fs_give((void **) &s); } } } } /* code_challenge generator */ void oauth2_login_hint(OAUTH2_S *oauth2, char *user) { OA2_type i, j, k; char *cv, *cv1, *cv2; if(!user || !*user) return; i = oauth2_find_extra_parameter(oauth2, "login_hint"); if(i == OA2_End) return; if(oauth2->param[i].value) fs_give((void **) &oauth2->param[i].value); oauth2->param[i].value = cpystr(user); } /* we generate something like a guid, but not care about * anything, but that it is really random. */ char *oauth2_generate_state(void) { char rv[37]; int i; rv[0] = '\0'; for(i = 0; i < 4; i++) sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256)); sprintf(rv + strlen(rv), "%c", '-'); for(i = 0; i < 2; i++) sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256)); sprintf(rv + strlen(rv), "%c", '-'); for(i = 0; i < 2; i++) sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256)); sprintf(rv + strlen(rv), "%c", '-'); for(i = 0; i < 2; i++) sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256)); sprintf(rv + strlen(rv), "%c", '-'); for(i = 0; i < 6; i++) sprintf(rv + strlen(rv), "%02x", (unsigned int) (random() % 256)); rv[36] = '\0'; return cpystr(rv); } char * xoauth2_server(char *server, char *tenant) { char *rv = NULL; if (server == NULL) return NULL; if(tenant){ char *s = cpystr(server); char *t = s, *u; int i; for(i = 0; t != NULL; i++){ t = strchr(t, '\001'); if(t != NULL) t++; } rv = fs_get((strlen(s) + i*(strlen(tenant)-1) + 1)*sizeof(char)); *rv = '\0'; for(u = t = s; t != NULL; i++){ t = strchr(t, '\001'); if (t != NULL) *t = '\0'; strcat(rv, u); if(t != NULL){ strcat(rv, tenant); *t++ = '\001'; } u = t; } fs_give((void **) &s); } else rv = cpystr(server); return rv; } JSON_S * oauth2_json_reply(OAUTH2_SERVER_METHOD_S RefreshMethod, OAUTH2_S *oauth2, int *status) { JSON_S *json = NULL; HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; HTTPSTREAM *stream = NIL; unsigned char *s; char *server = NULL; LOAD_HTTP_PARAMS(RefreshMethod, params); *status = 0; server = xoauth2_server(RefreshMethod.urlserver, oauth2->param[OA2_Tenant].value); if(strcmp(RefreshMethod.name, "POST") == 0 && ((stream = http_open(server)) != NULL) && ((s = http_post_param(stream, params)) != NULL)){ json = json_parse(s); fs_give((void **) &s); } *status = stream && stream->status ? stream->status->code : -1; if(stream) http_close(stream); if(server) fs_give((void **) &server); return json; } void mm_login_oauth2_c_client_method (NETMBX *mb, char *user, char *method, OAUTH2_S *oauth2, unsigned long trial, int *tryanother) { int status; char *s = NULL; JSON_S *json = NULL; mm_log("mm_login_oauth2_c_client_method()", (long) NIL); if(oauth2->param[OA2_Id].value == NULL || (oauth2->require_secret && oauth2->param[OA2_Secret].value == NULL)){ XOAUTH2_INFO_S *x; oauth2clientinfo_t ogci = (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL); mm_log("Setting up for next call. Attempting to ask client for client-id and client-secret.", (long) NIL); if(ogci && (x = (*ogci)(oauth2->name, user)) != NULL){ oauth2->param[OA2_Id].value = cpystr(x->client_id); oauth2->param[OA2_Secret].value = x->client_secret ? cpystr(x->client_secret) : NULL; if(oauth2->param[OA2_Tenant].value) fs_give((void **) &oauth2->param[OA2_Tenant].value); oauth2->param[OA2_Tenant].value = x->tenant ? cpystr(x->tenant) : NULL; free_xoauth2_info(&x); } } if(oauth2->param[OA2_Id].value == NULL || (oauth2->require_secret && oauth2->param[OA2_Secret].value == NULL)){ *tryanother = 1; oauth2_free_extra_values(*oauth2); mm_log("could not get client-id or client-secret required and empty.", (long) NIL); return; } mm_log("Got a client-id/client-secret to use.", (long) NIL); /* Do we have a method to execute? */ if (oauth2->first_time && oauth2->server_mthd[OA2_GetDeviceCode].name){ oauth2deviceinfo_t ogdi; mm_log("Attempting DEVICE method.", (long) NIL); json = oauth2_json_reply(oauth2->server_mthd[OA2_GetDeviceCode], oauth2, &status); if(json != NULL){ JSON_S *jx; json_assign ((void **) &oauth2->devicecode.device_code, json, "device_code", JString); json_assign ((void **) &oauth2->devicecode.user_code, json, "user_code", JString); json_assign ((void **) &oauth2->devicecode.verification_uri, json, "verification_uri", JString); if((jx = json_body_value(json, "expires_in")) != NULL) switch(jx->jtype){ case JString: oauth2->devicecode.expires_in = atoi((char *) jx->value); break; case JLong : oauth2->devicecode.expires_in = *(long *) jx->value; break; default : break; } if((jx = json_body_value(json, "interval")) != NULL) switch(jx->jtype){ case JString: oauth2->devicecode.interval = atoi((char *) jx->value); break; case JLong : oauth2->devicecode.interval = *(long *) jx->value; break; default : break; } json_assign ((void **) &oauth2->devicecode.message, json, "message", JString); json_free(&json); if(oauth2->devicecode.verification_uri && oauth2->devicecode.user_code){ ogdi = (oauth2deviceinfo_t) mail_parameters (NIL, GET_OA2DEVICEINFO, NIL); if(ogdi) (*ogdi)(oauth2, method); } mm_log("Got Json reply. Completed parsing.", (long) NIL); } return; } /* else check if we have a refresh token, and in that case use it */ if(oauth2->param[OA2_RefreshToken].value){ mm_log("Attempting to get access token with known refresh token.", (long) NIL); json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken], oauth2, &status); if(json != NULL){ JSON_S *jx; switch(status){ case HTTP_UNAUTHORIZED: mm_log("Client not authorized (wrong client-id?)", ERROR); oauth2->cancel_refresh_token++; break; case HTTP_OK: if(oauth2->access_token) fs_give((void **) &oauth2->access_token); json_assign ((void **) &oauth2->access_token, json, "access_token", JString); if((jx = json_body_value(json, "expires_in")) != NULL) switch(jx->jtype){ case JString: oauth2->expiration = time(0) + atol((char *) jx->value); break; case JLong : oauth2->expiration = time(0) + *(long *) jx->value; break; default : break; } oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */ mm_log("Got new refresh token.", (long) NIL); break; default : { char tmp[200]; char *err, *err_desc; jx = json_body_value(json, "error"); err = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "Unknown error"); jx = json_body_value(json, "error_description"); err_desc = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "No description"); sprintf(tmp, "Code %d: %.80s: %.80s", status, err, err_desc); mm_log (tmp, ERROR); if(err) fs_give((void **) &err); if(err_desc) fs_give((void **) &err_desc); oauth2->cancel_refresh_token++; } break; } json_free(&json); } return; } /* * else, we do not have a refresh token, nor an access token. * We need to start the process to get an access code. We use this * to get an access token and refresh token. */ { OAUTH2_SERVER_METHOD_S RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode]; HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; mm_log("Starting AUTHORIZE method. No refresh token nor access token found.", (long) NIL); LOAD_HTTP_PARAMS(RefreshMethod, params); if(strcmp(RefreshMethod.name, "GET") == 0){ char *server = xoauth2_server(RefreshMethod.urlserver, oauth2->param[OA2_Tenant].value); char *url = http_get_param_url(server, params); oauth2getaccesscode_t ogac = (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL); if(ogac) oauth2->param[OA2_Code].value = (*ogac)(url, method, oauth2, tryanother); if(server) fs_give((void **) &server); if(url) fs_give((void **) &url); } if(oauth2->param[OA2_Code].value){ json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status); if(json != NULL){ JSON_S *jx; switch(status){ case HTTP_OK : if(oauth2->param[OA2_RefreshToken].value) fs_give((void **) &oauth2->param[OA2_RefreshToken].value); json_assign ((void **) &oauth2->param[OA2_RefreshToken].value, json, "refresh_token", JString); if(oauth2->access_token) fs_give((void **) &oauth2->access_token); json_assign ((void **) &oauth2->access_token, json, "access_token", JString); if((jx = json_body_value(json, "expires_in")) != NULL) switch(jx->jtype){ case JString: oauth2->expiration = time(0) + atol((char *) jx->value); break; case JLong : oauth2->expiration = time(0) + *(long *) jx->value; break; default : break; } oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */ mm_log("Got new refresh and access token.", (long) NIL); break; case HTTP_BAD : default : { char tmp[200]; char *err, *err_desc; jx = json_body_value(json, "error"); err = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "Unknown error"); jx = json_body_value(json, "error_description"); err_desc = cpystr(jx && jx->jtype == JString ? (char *) jx->value : "No description"); sprintf(tmp, "Code %d: %.80s: %.80s", status, err, err_desc); mm_log (tmp, ERROR); if(err) fs_give((void **) &err); if(err_desc) fs_give((void **) &err_desc); oauth2->cancel_refresh_token++; } } json_free(&json); } } else mm_log("Failed to obtain authorization code. Cancelled by user?", (long) NIL); return; } } void oauth2deviceinfo_get_accesscode(void *inp, void *outp) { OAUTH2_DEVICEPROC_S *oad = (OAUTH2_DEVICEPROC_S *) inp; OAUTH2_S *oauth2 = oad->xoauth2; OAUTH2_DEVICECODE_S *dcode = &oauth2->devicecode; int done = 0, status, rv; JSON_S *json; mm_log("oauth2deviceinfo: getting accesscode.", (long) NIL); if(dcode->device_code && oauth2->param[OA2_DeviceCode].value == NULL) oauth2->param[OA2_DeviceCode].value = cpystr(dcode->device_code); rv = OA2_CODE_WAIT; /* wait by default */ json = oauth2_json_reply(oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode], oauth2, &status); if(json != NULL){ JSON_S *jx; char *error = NIL; switch(status){ case HTTP_BAD : json_assign ((void **) &error, json, "error", JString); if(!error) break; if(compare_cstring(error, "authorization_pending") == 0) rv = OA2_CODE_WAIT; else if(compare_cstring(error, "authorization_declined") == 0) rv = OA2_CODE_FAIL; else if(compare_cstring(error, "bad_verification_code") == 0) rv = OA2_CODE_FAIL; else if(compare_cstring(error, "expired_token") == 0) rv = OA2_CODE_FAIL; else /* keep waiting? */ rv = OA2_CODE_WAIT; mm_log(rv == OA2_CODE_FAIL ? error : "waiting for process to end.", rv == OA2_CODE_FAIL ? ERROR : (long) NIL); break; case HTTP_OK : if(oauth2->param[OA2_RefreshToken].value) fs_give((void **) &oauth2->param[OA2_RefreshToken].value); json_assign ((void **) &oauth2->param[OA2_RefreshToken].value, json, "refresh_token", JString); if(oauth2->access_token) fs_give((void **) &oauth2->access_token); json_assign ((void **) &oauth2->access_token, json, "access_token", JString); if((jx = json_body_value(json, "expires_in")) != NULL) switch(jx->jtype){ case JString: oauth2->expiration = time(0) + atol((char *) jx->value); break; case JLong : oauth2->expiration = time(0) + *(long *) jx->value; break; default : break; } rv = OA2_CODE_SUCCESS; oauth2->cancel_refresh_token = 0; /* do not cancel this token. It is good */ mm_log("Got new refresh and access token.", (long) NIL); break; default : { char tmp[100]; sprintf(tmp, "Oauth device Received Code %d.", status); mm_log (tmp, ERROR); oauth2->cancel_refresh_token++; } } json_free(&json); } *(int *)outp = rv; } XOAUTH2_INFO_S * new_xoauth2_info(void) { XOAUTH2_INFO_S *rv = fs_get(sizeof(XOAUTH2_INFO_S)); memset((void *) rv, 0, sizeof(XOAUTH2_INFO_S)); return rv; } void free_xoauth2_info(XOAUTH2_INFO_S **xp) { if(xp == NULL || *xp == NULL) return; if((*xp)->name) fs_give((void **) &(*xp)->name); if((*xp)->client_id) fs_give((void **) &(*xp)->client_id); if((*xp)->client_secret) fs_give((void **) &(*xp)->client_secret); if((*xp)->tenant) fs_give((void **) &(*xp)->tenant); if((*xp)->flow) fs_give((void **) &(*xp)->flow); if((*xp)->users) fs_give((void **) &(*xp)->users); fs_give((void **) xp); } XOAUTH2_INFO_S * copy_xoauth2_info(XOAUTH2_INFO_S *x) { XOAUTH2_INFO_S *y; if(x == NULL) return NULL; y = new_xoauth2_info(); if(x->name) y->name = cpystr(x->name); if(x->client_id) y->client_id = cpystr(x->client_id); if(x->client_secret) y->client_secret = cpystr(x->client_secret); if(x->tenant) y->tenant = cpystr(x->tenant); if(x->flow) y->flow = cpystr(x->flow); if(x->users) y->users = cpystr(x->users); return y; } int same_xoauth2_info(XOAUTH2_INFO_S x, XOAUTH2_INFO_S y) { int rv = 0; if(x.name && y.name && !strcmp((char *) x.name, (char *) y.name) && x.client_id && y.client_id && !strcmp(x.client_id, y.client_id) && ((!x.client_secret && !y.client_secret) || (x.client_secret && y.client_secret && !strcmp(x.client_secret, y.client_secret))) && ((!x.tenant && !y.tenant) || (x.tenant && y.tenant && !strcmp(x.tenant, y.tenant)))) rv = 1; return rv; } void free_xoauth2_info_list(XOAUTH2_INFO_S ***xp) { int i; if(xp == NULL || *xp == NULL || **xp == NULL) return; for(i = 0; (*xp)[i] != NULL; i++) free_xoauth2_info(&(*xp)[i]); fs_give((void **) xp); } /* This function does not create a refresh token and and * an access token, but uses an already known refresh token * to generate a refresh token on an ALREADY OPEN stream. * The assumption is that the user has already unlocked all * passwords and the app can access them from some source * (key chain/credentials/memory) to go through this * process seamlessly. */ void renew_accesstoken(MAILSTREAM *stream) { OAUTH2_S oauth2; NETMBX mb; char user[MAILTMPLEN]; int tryanother; unsigned long trial = 0; mm_log("renew_accesstoken().", (long) NIL); memset((void *) &oauth2, 0, sizeof(OAUTH2_S)); mail_valid_net_parse(stream->original_mailbox, &mb); user[0] = '\0'; mm_login_method (&mb, user, (void *) &oauth2, trial, stream->auth.name); if(oauth2.access_token) /* we need a new one */ fs_give((void **) &oauth2.access_token); if(stream->auth.expiration == 0){ stream->auth.expiration = oauth2.expiration; if(oauth2.param[OA2_RefreshToken].value) fs_give((void **) &oauth2.param[OA2_RefreshToken].value); return; } oauth2.param[OA2_State].value = oauth2_generate_state(); mm_login_oauth2_c_client_method (&mb, user, stream->auth.name, &oauth2, trial, &tryanother); if(oauth2.access_token) mm_login_method (&mb, user, (void *) &oauth2, trial, stream->auth.name); stream->auth.expiration = oauth2.expiration; oauth2_free_extra_values(oauth2); }