diff options
Diffstat (limited to 'imap/src/c-client/auth_oa2.c')
-rw-r--r-- | imap/src/c-client/auth_oa2.c | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/imap/src/c-client/auth_oa2.c b/imap/src/c-client/auth_oa2.c new file mode 100644 index 00000000..d77b0102 --- /dev/null +++ b/imap/src/c-client/auth_oa2.c @@ -0,0 +1,359 @@ +/* ======================================================================== + * Copyright 2018 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 + * + * + * ======================================================================== + */ + +long auth_oauth2_client (authchallenge_t challenger,authrespond_t responder, + char *service,NETMBX *mb,void *stream, + unsigned long *trial,char *user); + +void mm_login_oauth2_c_client_method (NETMBX *, char *, OAUTH2_S *, unsigned long, int *); + +char *oauth2_generate_state(void); + +AUTHENTICATOR auth_oa2 = { + AU_HIDE, /* hidden */ + OA2NAME, /* authenticator name */ + NIL, /* always valid */ + auth_oauth2_client, /* client method */ + NIL, /* server method */ + NIL /* next authenticator */ +}; + +#define OAUTH2_USER "user=" +#define OAUTH2_BEARER "auth=Bearer " + +/* we generate something like a guid, but not care about + * anything, but that it is really random. + */ +char *oauth2_generate_state(void) +{ + char rv[36]; + int i; + + rv[0] = '\0'; + for(i = 0; i < 4; i++) + sprintf(rv + strlen(rv), "%x", random() % 256); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 2; i++) + sprintf(rv + strlen(rv), "%x", random() % 256); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 2; i++) + sprintf(rv + strlen(rv), "%x", random() % 256); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 2; i++) + sprintf(rv + strlen(rv), "%x", random() % 256); + sprintf(rv + strlen(rv), "%c", '-'); + for(i = 0; i < 6; i++) + sprintf(rv + strlen(rv), "%x", random() % 256); + rv[36] = '\0'; + return cpystr(rv); +} + + +/* Client authenticator + * Accepts: challenger function + * responder function + * SASL service name + * parsed network mailbox structure + * stream argument for functions + * pointer to current trial count + * returned user name + * Returns: T if success, NIL otherwise, number of trials incremented if retry + */ + +long auth_oauth2_client (authchallenge_t challenger,authrespond_t responder, + char *service,NETMBX *mb,void *stream, + unsigned long *trial,char *user) +{ + char *u; + void *challenge; + unsigned long clen; + long ret = NIL; + OAUTH2_S oauth2; + int tryanother = 0; /* try another authentication method */ + + memset((void *) &oauth2, 0, sizeof(OAUTH2_S)); + /* snarl if not SSL/TLS session */ + if (!mb->sslflag && !mb->tlsflag) + mm_log ("SECURITY PROBLEM: insecure server advertised AUTH=XOAUTH2",WARN); + + /* get initial (empty) challenge */ + if ((challenge = (*challenger) (stream,&clen)) != NULL) { + fs_give ((void **) &challenge); + if (clen) { /* abort if challenge non-empty */ + mm_log ("Server bug: non-empty initial XOAUTH2 challenge",WARN); + (*responder) (stream,NIL,0); + ret = LONGT; /* will get a BAD response back */ + } + + /* + * the call to mm_login_method is supposed to return the username + * and access token. If this is not known by the application, then + * we call our internal functions to get a refresh token, access token + * and expiration time. + * + * Programmers note: We always call mm_login_method at least once. + * The first call is done with empty parameters and it indicates + * we are asking the application to load it the best it can. Then + * the application returns the loaded value. If we get it fully loaded + * we use the value, but if we don't get it fully loaded, we call + * our internal functions to try to fully load it. + * + * If in the internal call we get it loaded, then we use these values + * to log in. At this time we call the app to send back the loaded values + * so it can save them for the next time we call. This is done in a + * second call to mm_login_method. If we do not get oauth2 back with + * fully loaded values we cancel authentication completely. If the + * user cannot load this variable, then the user, through the client, + * should disable XOAUTH2 as an authentication method and try a new one. + * + * If we make our internal mm_login_oauth2_c_client_method call, + * we might still need to call the client to get the access token, + * this is done through a callback declared by the client. If we need + * that information, but the callback is not declared, this process + * will fail, so we will check if that call is declared as soon as we + * know we should start it, and we will only start it if this callback + * is declared. + * + * We start this process by calling the client and loading oauth2 + * with the required information as best as we can. + */ + + mm_login_method (mb, user, (void *) &oauth2, *trial, OA2NAME); + + if(oauth2.param[OA2_State].value) + fs_give((void **) &oauth2.param[OA2_State].value); + + oauth2.param[OA2_State].value = oauth2_generate_state(); + + /* + * If we did not get an access token, try to get one through + * our internal functions + */ + if(oauth2.name && oauth2.access_token == NIL){ + char *RefreshToken = NIL; + + if(oauth2.param[OA2_RefreshToken].value) + RefreshToken = cpystr(oauth2.param[OA2_RefreshToken].value); + + mm_login_oauth2_c_client_method (mb, user, &oauth2, *trial, &tryanother); + + /* + * if we got an access token from the c_client_method call, + * or somehow there was a change in the refresh token, return + * it to the client so that it will save it. + */ + + if(!tryanother + && (oauth2.access_token + || (!RefreshToken && oauth2.param[OA2_RefreshToken].value) + || (RefreshToken && oauth2.param[OA2_RefreshToken].value + && strcmp(RefreshToken, oauth2.param[OA2_RefreshToken].value)))) + mm_login_method (mb, user, (void *) &oauth2, *trial, OA2NAME); + } + + /* empty challenge or user requested abort or client does not have info */ + if(!oauth2.access_token) { + (*responder) (stream,NIL,0); + *trial = 0; /* cancel subsequent attempts */ + ret = LONGT; /* will get a BAD response back */ + } + else { + unsigned long rlen = strlen(OAUTH2_USER) + strlen(user) + + strlen(OAUTH2_BEARER) + strlen(oauth2.access_token) + 1 + 2; + char *response = (char *) fs_get (rlen); + char *t = response; /* copy authorization id */ + for (u = OAUTH2_USER; *u; *t++ = *u++); + for (u = user; *u; *t++ = *u++); + *t++ = '\001'; /* delimiting ^A */ + for (u = OAUTH2_BEARER; *u; *t++ = *u++); + for (u = oauth2.access_token; *u; *t++ = *u++); + *t++ = '\001'; /* delimiting ^A */ + *t++ = '\001'; /* delimiting ^A */ + if ((*responder) (stream,response,rlen)) { + if ((challenge = (*challenger) (stream,&clen)) != NULL) + fs_give ((void **) &challenge); + else { + ++*trial; /* can try again if necessary */ + ret = *trial < 3 ? LONGT : NIL; /* check the authentication */ + /* When the Access Token expires we fail once, but after we get + * a new one, we should succeed at the second attempt. If the + * Refresh Token has expired somehow, we invalidate it if we + * reach *trial to 3. This forces the process to restart later on. + */ + if(*trial == 3){ + if(oauth2.param[OA2_State].value) + fs_give((void **) &oauth2.param[OA2_State].value); + fs_give((void **) &oauth2.param[OA2_RefreshToken].value); + fs_give((void **) &oauth2.access_token); + oauth2.expiration = 0L; + } + } + } + fs_give ((void **) &response); + } + } + if (!ret || !oauth2.name || tryanother) + *trial = 65535; /* don't retry if bad protocol */ + return ret; +} + +/* + * The code above is enough to implement XOAUTH2, all one needs is the username + * and access token and give it to the function above. However, normal users cannot + * be expected to get the access token, so we ask the client to help with getting + * the access token, refresh token and expire values, so the code below is written + * to help with that. + */ + +#include "http.h" +#include "json.h" + +void +mm_login_oauth2_c_client_method (NETMBX *mb, char *user, + OAUTH2_S *oauth2, unsigned long trial, int *tryanother) +{ + int i; + HTTP_PARAM_S params[OAUTH2_PARAM_NUMBER]; + OAUTH2_SERVER_METHOD_S RefreshMethod; + char *s = NULL; + JSON_S *json = NULL; + + if(oauth2->param[OA2_Id].value == NULL + || oauth2->param[OA2_Secret].value == NULL){ + /* + * We need to implement client-side entering client_id and + * client_secret, and other parameters. In the mean time, bail out. + */ + return; + } + + /* first check if we have a refresh token, and in that case use it */ + if(oauth2->param[OA2_RefreshToken].value){ + + RefreshMethod = oauth2->server_mthd[OA2_GetAccessTokenFromRefreshToken]; + for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ + OA2_type j = RefreshMethod.params[i]; + params[i].name = oauth2->param[j].name; + params[i].value = oauth2->param[j].value; + } + params[i].name = params[i].value = NULL; + + if(strcmp(RefreshMethod.name, "POST") == 0) + s = http_post_param(RefreshMethod.urlserver, params); + else if(strcmp(RefreshMethod.name, "POST2") == 0) + s = http_post_param2(RefreshMethod.urlserver, params); + + if(s){ + unsigned char *t, *u; + if((t = strstr(s, "\r\n\r\n")) && (u = strchr(t, '{'))) + json = json_parse(&u); + fs_give((void **) &s); + } + + if(json != NULL){ + JSON_X *jx; + + jx = json_body_value(json, "access_token"); + if(jx && jx->jtype == JString) + oauth2->access_token = cpystr((char *) jx->value); + + jx = json_body_value(json, "expires_in"); + if(jx){ + if(jx->jtype == JString){ + unsigned long *l = fs_get(sizeof(unsigned long)); + *l = atol((char *) jx->value); + fs_give(&jx->value); + jx->value = (void *) l; + jx->jtype = JLong; + } + if(jx->jtype == JLong) + oauth2->expiration = time(0) + *(unsigned long *) jx->value; + } + + 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. + */ + { + RefreshMethod = oauth2->server_mthd[OA2_GetAccessCode]; + for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ + OA2_type j = RefreshMethod.params[i]; + params[i].name = oauth2->param[j].name; + params[i].value = oauth2->param[j].value; + } + params[i].name = params[i].value = NULL; + + if(strcmp(RefreshMethod.name, "GET") == 0){ + char *url = http_get_param_url(RefreshMethod.urlserver, params); + oauth2getaccesscode_t ogac = + (oauth2getaccesscode_t) mail_parameters (NIL, GET_OA2CLIENTGETACCESSCODE, NIL); + + if(ogac) + oauth2->param[OA2_Code].value = (*ogac)(url, oauth2, tryanother); + } + + if(oauth2->param[OA2_Code].value){ + RefreshMethod = oauth2->server_mthd[OA2_GetAccessTokenFromAccessCode]; + for(i = 0; RefreshMethod.params[i] != OA2_End; i++){ + OA2_type j = RefreshMethod.params[i]; + params[i].name = oauth2->param[j].name; + params[i].value = oauth2->param[j].value; + } + params[i].name = params[i].value = NULL; + + if(strcmp(RefreshMethod.name, "POST") == 0) + s = http_post_param(RefreshMethod.urlserver, params); + else if(strcmp(RefreshMethod.name, "POST2") == 0) + s = http_post_param2(RefreshMethod.urlserver, params); + + if(s){ + unsigned char *t, *u; + if((t = strstr(s, "\r\n\r\n")) && (u = strchr(t, '{'))) + json = json_parse(&u); + fs_give((void **) &s); + } + + if(json != NULL){ + JSON_X *jx; + + jx = json_body_value(json, "refresh_token"); + if(jx && jx->jtype == JString) + oauth2->param[OA2_RefreshToken].value = cpystr((char *) jx->value); + + jx = json_body_value(json, "access_token"); + if(jx && jx->jtype == JString) + oauth2->access_token = cpystr((char *) jx->value); + + jx = json_body_value(json, "expires_in"); + if(jx){ + if(jx->jtype == JString){ + unsigned long *l = fs_get(sizeof(unsigned long)); + *l = atol((char *) jx->value); + fs_give(&jx->value); + jx->value = (void *) l; + jx->jtype = JLong; + } + if(jx->jtype == JLong) + oauth2->expiration = time(0) + *(unsigned long *) jx->value; + } + json_free(&json); + } + } + return; + } +} |