/* 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(db->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
};


