diff options
Diffstat (limited to 'src/auth/mech-oauth2.c')
-rw-r--r-- | src/auth/mech-oauth2.c | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/src/auth/mech-oauth2.c b/src/auth/mech-oauth2.c new file mode 100644 index 0000000..dae5632 --- /dev/null +++ b/src/auth/mech-oauth2.c @@ -0,0 +1,339 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "safe-memset.h" +#include "str.h" +#include "mech.h" +#include "passdb.h" +#include "oauth2.h" +#include "json-parser.h" +#include <ctype.h> + +struct oauth2_auth_request { + struct auth_request auth; + bool failed; +}; + +static bool oauth2_find_oidc_url(struct auth_request *req, const char **url_r) +{ + struct auth_passdb *db = req->passdb; + if (req->openid_config_url != NULL) { + *url_r = req->openid_config_url; + return TRUE; + } + + /* keep looking until you get a value */ + for (; db != NULL; db = db->next) { + if (strcmp(db->passdb->iface.name, "oauth2") == 0) { + const char *url = + passdb_oauth2_get_oidc_url(req->passdb->passdb); + if (url == NULL || *url == '\0') + continue; + *url_r = url; + return TRUE; + } + } + + return FALSE; +} + +/* RFC5801 based unescaping */ +static bool oauth2_unescape_username(const char *in, const char **username_r) +{ + string_t *out; + out = t_str_new(64); + for (; *in != '\0'; in++) { + if (in[0] == ',') + return FALSE; + if (in[0] == '=') { + if (in[1] == '2' && in[2] == 'C') + str_append_c(out, ','); + else if (in[1] == '3' && in[2] == 'D') + str_append_c(out, '='); + else + return FALSE; + in += 2; + } else { + str_append_c(out, *in); + } + } + *username_r = str_c(out); + return TRUE; +} + +static void oauth2_verify_callback(enum passdb_result result, + const char *const *error_fields, + struct auth_request *request) +{ + const char *oidc_url; + + i_assert(result == PASSDB_RESULT_OK || error_fields != NULL); + switch (result) { + case PASSDB_RESULT_OK: + auth_request_success(request, "", 0); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + request->internal_failure = TRUE; + /* fall through */ + default: + /* we could get new token after this */ + if (request->mech_password != NULL) + request->mech_password = NULL; + string_t *error = t_str_new(64); + str_append_c(error, '{'); + for (unsigned int i = 0; error_fields[i] != NULL; i += 2) { + i_assert(error_fields[i+1] != NULL); + if (i > 0) + str_append_c(error, ','); + str_append_c(error, '"'); + json_append_escaped(error, error_fields[i]); + str_append(error, "\":\""); + json_append_escaped(error, error_fields[i+1]); + str_append_c(error, '"'); + } + /* FIXME: HORRIBLE HACK - REMOVE ME!!! + It is because the mech has not been implemented properly + that we need to pass the config url in this strange way. + + This **must** be removed from here and db-oauth2 once the + validation result et al is handled here. + */ + if (oauth2_find_oidc_url(request, &oidc_url)) { + if (str_len(error) > 0) + str_append_c(error, ','); + str_printfa(error, "\"openid-configuration\":\""); + json_append_escaped(error, oidc_url); + str_append_c(error, '"'); + } + str_append_c(error, '}'); + auth_request_fail_with_reply(request, str_data(error), str_len(error)); + break; + } +} + +static void mech_oauth2_verify_token(struct auth_request *request, + const char *token, + enum passdb_result result, + verify_plain_callback_t callback) +{ + i_assert(token != NULL); + if (result != PASSDB_RESULT_OK) { + request->passdb_result = result; + request->failed = TRUE; + } + auth_request_verify_plain(request, token, callback); +} + +static void +xoauth2_verify_callback(enum passdb_result result, struct auth_request *request) +{ + const char *const error_fields[] = { + "status", "401", + "schemes", "bearer", + "scope", "mail", + NULL + }; + oauth2_verify_callback(result, error_fields, request); +} + +static void +oauthbearer_verify_callback(enum passdb_result result, struct auth_request *request) +{ + const char *error_fields[] = { + "status", "invalid_token", + NULL + }; + oauth2_verify_callback(result, error_fields, request); +} + +/* Input syntax: + user=Username^Aauth=Bearer token^A^A +*/ +static void +mech_xoauth2_auth_continue(struct auth_request *request, + const unsigned char *data, + size_t data_size) +{ + /* split the data from ^A */ + bool user_given = FALSE; + const char *error; + const char *token = NULL; + const char *const *ptr; + const char *username; + const char *const *fields = + t_strsplit(t_strndup(data, data_size), "\x01"); + for(ptr = fields; *ptr != NULL; ptr++) { + if (str_begins(*ptr, "user=")) { + /* xoauth2 does not require unescaping because the data + format does not contain anything to escape */ + username = (*ptr)+5; + user_given = TRUE; + } else if (str_begins(*ptr, "auth=")) { + const char *value = (*ptr)+5; + if (strncasecmp(value, "bearer ", 7) == 0 && + oauth2_valid_token(value+7)) { + token = value+7; + } else { + e_info(request->mech_event, + "Invalid continued data"); + xoauth2_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + } + } + /* do not fail on unexpected fields */ + } + + if (user_given && !auth_request_set_username(request, username, &error)) { + e_info(request->mech_event, + "%s", error); + xoauth2_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + return; + } + + if (user_given && token != NULL) + mech_oauth2_verify_token(request, token, PASSDB_RESULT_OK, + xoauth2_verify_callback); + else { + e_info(request->mech_event, "Username or token missing"); + xoauth2_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + } +} + +/* Input syntax for data: + gs2flag,a=username,^Afield=...^Afield=...^Aauth=Bearer token^A^A +*/ +static void +mech_oauthbearer_auth_continue(struct auth_request *request, + const unsigned char *data, + size_t data_size) +{ + bool user_given = FALSE; + const char *error; + const char *username; + const char *const *ptr; + /* split the data from ^A */ + const char **fields = + t_strsplit(t_strndup(data, data_size), "\x01"); + const char *token = NULL; + /* ensure initial field is OK */ + if (*fields == NULL || *(fields[0]) == '\0') { + e_info(request->mech_event, + "Invalid continued data"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + } + + /* the first field is specified by RFC5801 as gs2-header */ + for(ptr = t_strsplit_spaces(fields[0], ","); *ptr != NULL; ptr++) { + switch(*ptr[0]) { + case 'f': + e_info(request->mech_event, + "Client requested non-standard mechanism"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + case 'p': + /* channel binding is not supported */ + e_info(request->mech_event, + "Client requested and used channel-binding"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + case 'n': + case 'y': + /* we don't need to use channel-binding */ + continue; + case 'a': /* authzid */ + if ((*ptr)[1] != '=' || + !oauth2_unescape_username((*ptr)+2, &username)) { + e_info(request->mech_event, + "Invalid username escaping"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + } else { + user_given = TRUE; + } + break; + default: + e_info(request->mech_event, + "Invalid gs2-header in request"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + } + } + + for(ptr = fields; *ptr != NULL; ptr++) { + if (str_begins(*ptr, "auth=")) { + const char *value = (*ptr)+5; + if (strncasecmp(value, "bearer ", 7) == 0 && + oauth2_valid_token(value+7)) { + token = value+7; + } else { + e_info(request->mech_event, + "Invalid continued data"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + } + } + /* do not fail on unexpected fields */ + } + if (user_given && !auth_request_set_username(request, username, &error)) { + e_info(request->mech_event, + "%s", error); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + return; + } + if (user_given && token != NULL) + mech_oauth2_verify_token(request, token, PASSDB_RESULT_OK, + oauthbearer_verify_callback); + else { + e_info(request->mech_event, "Missing username or token"); + oauthbearer_verify_callback(PASSDB_RESULT_PASSWORD_MISMATCH, + request); + } +} + +static struct auth_request *mech_oauth2_auth_new(void) +{ + struct oauth2_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"oauth2_auth_request", 2048); + request = p_new(pool, struct oauth2_auth_request, 1); + request->auth.pool = pool; + return &request->auth; +} + +const struct mech_module mech_oauthbearer = { + "OAUTHBEARER", + + /* while this does not transfer plaintext password, + the token is still considered as password */ + .flags = MECH_SEC_PLAINTEXT, + .passdb_need = 0, + + mech_oauth2_auth_new, + mech_generic_auth_initial, + mech_oauthbearer_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_xoauth2 = { + "XOAUTH2", + + .flags = MECH_SEC_PLAINTEXT, + .passdb_need = 0, + + mech_oauth2_auth_new, + mech_generic_auth_initial, + mech_xoauth2_auth_continue, + mech_generic_auth_free +}; + + |