diff options
Diffstat (limited to 'imap/src')
-rw-r--r-- | imap/src/c-client/auth_bea.c | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/imap/src/c-client/auth_bea.c b/imap/src/c-client/auth_bea.c new file mode 100644 index 00000000..683d769f --- /dev/null +++ b/imap/src/c-client/auth_bea.c @@ -0,0 +1,351 @@ +/* ======================================================================== + * Copyright 2020 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_oauthbearer_client (authchallenge_t challenger,authrespond_t responder, + char *service,NETMBX *mb,void *stream, unsigned long port, + unsigned long *trial,char *user); +#ifndef HTTP_OAUTH2_INCLUDED +void mm_login_oauth2_c_client_method (NETMBX *, char *, char *, OAUTH2_S *, unsigned long, int *); +#endif /* HTTP_OAUTH2_INCLUDED */ + +AUTHENTICATOR auth_bea = { + AU_HIDE, /* hidden */ + BEARERNAME, /* authenticator name */ + NIL, /* always valid */ + auth_oauthbearer_client, /* client method */ + NIL, /* server method */ + NIL /* next authenticator */ +}; + +#define BEARER_ACCOUNT "n,a=" +#ifndef OAUTH2_BEARER +#define OAUTH2_BEARER "auth=Bearer " +#endif +#define BEARER_HOST "host=" +#define BEARER_PORT "port=" + +#ifndef OAUTH2_GENERATE_STATE +#define OAUTH2_GENERATE_STATE +char *oauth2_generate_state(void); + +/* 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), "%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); +} +#endif /* OAUTH2_GENERATE_STATE */ + +/* 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_oauthbearer_client (authchallenge_t challenger,authrespond_t responder, + char *service,NETMBX *mb,void *stream, unsigned long port, + 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=OAUTHBEARER",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 OAUTHBEARER challenge",WARN); + (*responder) (stream,NIL,0); + ret = LONGT; /* will get a BAD response back */ + } + + mm_login_method (mb, user, (void *) &oauth2, *trial, BEARERNAME); + + 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, BEARERNAME, &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, BEARERNAME); + } + + /* 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 { + char ports[10]; + unsigned long rlen; + char *t, *response; + + sprintf(ports, "%lu", port); + rlen = strlen(BEARER_ACCOUNT) + strlen(user) + 1 + 1 + + strlen(BEARER_HOST) + strlen(mb->orighost) + 1 + + strlen(BEARER_PORT) + strlen(ports) + 1 + + strlen(OAUTH2_BEARER) + strlen(oauth2.access_token) + 2; + t = response = (char *) fs_get (rlen); + for (u = BEARER_ACCOUNT; *u; *t++ = *u++); + for (u = user; *u; *t++ = *u++); + *t++ = ','; + *t++ = '\001'; /* delimiting ^A */ + for (u = BEARER_HOST; *u; *t++ = *u++); + for (u = mb->orighost; *u; *t++ = *u++); + *t++ = '\001'; /* delimiting ^A */ + for (u = BEARER_PORT; *u; *t++ = *u++); + for (u = ports; *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; +} + +#ifndef HTTP_OAUTH2_INCLUDED +#define HTTP_OAUTH2_INCLUDED +/* + * The code above is enough to implement OAUTHBEARER, 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, char *method, + 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){ + oauth2clientinfo_t ogci = + (oauth2clientinfo_t) mail_parameters (NIL, GET_OA2CLIENTINFO, NIL); + + if(ogci) (*ogci)(oauth2->name, &oauth2->param[OA2_Id].value, + &oauth2->param[OA2_Secret].value); + } + + if(oauth2->param[OA2_Id].value == NULL || oauth2->param[OA2_Secret].value == NULL) + 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, BEARERNAME, 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; + } +} +#endif /* HTTP_OAUTH2_INCLUDED */ |