changeset 21578:8b9d500c4917

auth: Add xoauth2 and oauthbearer mechanisms
author Aki Tuomi <aki.tuomi@dovecot.fi>
date Sat, 04 Feb 2017 23:56:04 +0200
parents 5c390ae4f640
children 0006d9824c80
files src/auth/Makefile.am src/auth/mech-oauth2.c src/auth/mech.c
diffstat 3 files changed, 286 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/auth/Makefile.am	Mon Feb 06 12:56:27 2017 +0200
+++ b/src/auth/Makefile.am	Sat Feb 04 23:56:04 2017 +0200
@@ -106,6 +106,7 @@
 	mech-apop.c \
 	mech-winbind.c \
 	mech-dovecot-token.c \
+	mech-oauth2.c \
 	passdb.c \
 	passdb-blocking.c \
 	passdb-bsdauth.c \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/auth/mech-oauth2.c	Sat Feb 04 23:56:04 2017 +0200
@@ -0,0 +1,279 @@
+/* Copyright (c) 2017 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 <ctype.h>
+
+struct oauth2_auth_request {
+	struct auth_request auth;
+	bool failed;
+};
+
+/* 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 *error,
+				   struct auth_request *request)
+{
+	struct oauth2_auth_request *oauth2_req =
+			(struct oauth2_auth_request*)request;
+
+	i_assert(result == PASSDB_RESULT_OK || error != NULL);
+	switch (result) {
+	case PASSDB_RESULT_OK:
+		auth_request_success(request, "", 0);
+		break;
+	case PASSDB_RESULT_INTERNAL_FAILURE:
+		auth_request_internal_failure(request);
+		break;
+	default:
+		/* we could get new token after this */
+		if (request->mech_password)
+			request->mech_password = NULL;
+		auth_request_handler_reply_continue(request, error, strlen(error));
+		oauth2_req->failed = TRUE;
+		break;
+	}
+}
+
+static void
+xoauth2_verify_callback(enum passdb_result result, struct auth_request *request)
+{
+	const char *error =
+		"{\"status\":\"401\",\"schemes\":\"bearer\",\"scope\":\"mail\"}";
+	oauth2_verify_callback(result, error, request);
+}
+
+static void
+oauthbearer_verify_callback(enum passdb_result result, struct auth_request *request)
+{
+	const char *error =
+		"{\"status\":\"invalid_token\"}";
+	oauth2_verify_callback(result, error, 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)
+{
+	struct oauth2_auth_request *oauth2_req =
+			(struct oauth2_auth_request*)request;
+
+	/* Specification says that client is sent "invalid token" challenge
+	   which the client is supposed to ack with empty response */
+	if (oauth2_req->failed) {
+		auth_request_fail(request);
+		return;
+	}
+
+	/* split the data from ^A */
+	bool user_given = FALSE;
+	const char *error;
+	const char *token = NULL;
+	const char *const *ptr;
+	const char *const *fields =
+		t_strsplit(t_strndup(data, data_size), "\x01");
+	for(ptr = fields; *ptr != NULL; ptr++) {
+		if (strncmp(*ptr,"user=", 5) == 0) {
+			/* xoauth2 does not require unescaping because the data
+			   format does not contain anything to escape */
+			const char *username = (*ptr)+5;
+			if (username == NULL) {
+				auth_request_log_info(request, AUTH_SUBSYS_MECH,
+						      "Invalid username");
+				auth_request_fail(request);
+				return;
+			}
+			if (!auth_request_set_username(request, username, &error)) {
+				auth_request_log_info(request, AUTH_SUBSYS_MECH,
+						      "%s", error);
+				auth_request_fail(request);
+				return;
+			}
+			user_given = TRUE;
+		} else if (strncmp(*ptr,"auth=", 5) == 0) {
+			const char *value = (*ptr)+5;
+			if (strncasecmp(value, "bearer ", 7) == 0 &&
+			    oauth2_valid_token(value+7)) {
+				token = value+7;
+			} else {
+				auth_request_log_info(request, AUTH_SUBSYS_MECH,
+						      "Invalid continued data");
+				auth_request_fail(request);
+				return;
+			}
+		}
+		/* do not fail on unexpected fields */
+	}
+
+	if (user_given && token != NULL)
+		auth_request_verify_plain(request, token,
+					  xoauth2_verify_callback);
+	else {
+		auth_request_log_info(request, AUTH_SUBSYS_MECH, "invalid input");
+		auth_request_fail(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)
+{
+	struct oauth2_auth_request *oauth2_req =
+			(struct oauth2_auth_request*)request;
+
+	if (oauth2_req->failed) {
+		auth_request_fail(request);
+		return;
+	}
+
+	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') {
+		auth_request_log_info(request, AUTH_SUBSYS_MECH,
+				      "Invalid continued data");
+		auth_request_fail(request);
+		return;
+	}
+
+	/* the first field is specified by RFC5801 as gs2-header */
+	for(ptr = t_strsplit(fields[0], ","); *ptr != NULL; ptr++) {
+		switch(*ptr[0]) {
+		case 'f':
+			auth_request_log_info(request, AUTH_SUBSYS_MECH,
+					      "Client requested non-standard mechanism");
+			auth_request_fail(request);
+			return;
+		case 'p':
+			/* channel binding is not supported */
+			auth_request_log_info(request, AUTH_SUBSYS_MECH,
+					      "Client requested and used channel-binding");
+			auth_request_fail(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)) {
+				 auth_request_log_info(request, AUTH_SUBSYS_MECH,
+						       "Invalid input");
+				 auth_request_fail(request);
+				 return;
+			} else if (!auth_request_set_username(request, username, &error)) {
+				auth_request_log_info(request, AUTH_SUBSYS_MECH,
+						      "%s", error);
+				
+			}
+		default:
+			auth_request_log_info(request, AUTH_SUBSYS_MECH,
+					      "Invalid gs2-header in request");
+			auth_request_fail(request);
+			return;
+		}
+	}
+
+	for(ptr = fields; *ptr != NULL; ptr++) {
+		if (strncmp(*ptr,"auth=", 5) == 0) {
+			const char *value = (*ptr)+5;
+			if (strncasecmp(value, "bearer ", 7) == 0 &&
+			    oauth2_valid_token(value+7)) {
+				token = value+7;
+			} else {
+				auth_request_log_info(request, AUTH_SUBSYS_MECH,
+						      "Invalid continued data");
+				auth_request_fail(request);
+				return;
+			}
+		}
+		/* do not fail on unexpected fields */
+	}
+	if (user_given && token != NULL)
+		auth_request_verify_plain(request, token,
+					  oauthbearer_verify_callback);
+	else {
+		auth_request_log_info(request, AUTH_SUBSYS_MECH, "invalid input");
+		auth_request_fail(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
+};
+
+
--- a/src/auth/mech.c	Mon Feb 06 12:56:27 2017 +0200
+++ b/src/auth/mech.c	Sat Feb 04 23:56:04 2017 +0200
@@ -82,6 +82,8 @@
 #endif
 extern const struct mech_module mech_winbind_ntlm;
 extern const struct mech_module mech_winbind_spnego;
+extern const struct mech_module mech_oauthbearer;
+extern const struct mech_module mech_xoauth2;
 
 static void mech_register_add(struct mechanisms_register *reg,
 			      const struct mech_module *mech)
@@ -211,6 +213,8 @@
 #ifdef BUILTIN_GSSAPI
 	mech_register_module(&mech_gssapi);
 #endif
+	mech_register_module(&mech_oauthbearer);
+	mech_register_module(&mech_xoauth2);
 }
 
 void mech_deinit(const struct auth_settings *set)
@@ -238,4 +242,6 @@
 #ifdef BUILTIN_GSSAPI
 	mech_unregister_module(&mech_gssapi);
 #endif
+	mech_unregister_module(&mech_oauthbearer);
+	mech_unregister_module(&mech_xoauth2);
 }