summaryrefslogtreecommitdiffstats
path: root/src/auth/db-oauth2.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/auth/db-oauth2.c')
-rw-r--r--src/auth/db-oauth2.c915
1 files changed, 915 insertions, 0 deletions
diff --git a/src/auth/db-oauth2.c b/src/auth/db-oauth2.c
new file mode 100644
index 0000000..b36a4ce
--- /dev/null
+++ b/src/auth/db-oauth2.c
@@ -0,0 +1,915 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "env-util.h"
+#include "var-expand.h"
+#include "settings.h"
+#include "oauth2.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "iostream-ssl.h"
+#include "auth-request.h"
+#include "auth-settings.h"
+#include "passdb.h"
+#include "passdb-template.h"
+#include "llist.h"
+#include "db-oauth2.h"
+#include "dcrypt.h"
+#include "dict.h"
+
+#include <stddef.h>
+
+struct passdb_oauth2_settings {
+ /* tokeninfo endpoint, format https://endpoint/somewhere?token= */
+ const char *tokeninfo_url;
+ /* password grant endpoint, format https://endpoint/somewhere */
+ const char *grant_url;
+ /* introspection endpoint, format https://endpoint/somewhere */
+ const char *introspection_url;
+ /* expected scope, optional */
+ const char *scope;
+ /* mode of introspection, one of get, get-auth, post
+ - get: append token to url
+ - get-auth: send token with header Authorization: Bearer token
+ - post: send token=<token> as POST request
+ */
+ const char *introspection_mode;
+ /* normalization var-expand template for username, defaults to %Lu */
+ const char *username_format;
+ /* name of username attribute to lookup, mandatory */
+ const char *username_attribute;
+ /* name of account is active attribute, optional */
+ const char *active_attribute;
+ /* expected active value for active attribute, optional */
+ const char *active_value;
+ /* client identificator for oauth2 server */
+ const char *client_id;
+ /* not really used, but have to present by oauth2 specs */
+ const char *client_secret;
+ /* template to expand into passdb */
+ const char *pass_attrs;
+ /* template to expand into key path, turns on local validation support */
+ const char *local_validation_key_dict;
+ /* valid token issuers */
+ const char *issuers;
+ /* The URL for a document following the OpenID Provider Configuration
+ Information schema, see
+
+ https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2
+ */
+ const char *openid_configuration_url;
+
+ /* TLS options */
+ const char *tls_ca_cert_file;
+ const char *tls_ca_cert_dir;
+ const char *tls_cert_file;
+ const char *tls_key_file;
+ const char *tls_cipher_suite;
+
+ /* HTTP rawlog directory */
+ const char *rawlog_dir;
+
+ /* HTTP client options */
+ unsigned int timeout_msecs;
+ unsigned int max_idle_time_msecs;
+ unsigned int max_parallel_connections;
+ unsigned int max_pipelined_requests;
+ bool tls_allow_invalid_cert;
+
+ bool debug;
+ /* Should introspection be done even if not necessary */
+ bool force_introspection;
+ /* Should we send service and local/remote endpoints as X-Dovecot-Auth headers */
+ bool send_auth_headers;
+ bool use_grant_password;
+};
+
+struct db_oauth2 {
+ struct db_oauth2 *prev,*next;
+
+ pool_t pool;
+
+ const char *config_path;
+ struct passdb_oauth2_settings set;
+ struct http_client *client;
+ struct passdb_template *tmpl;
+ struct oauth2_settings oauth2_set;
+
+ struct db_oauth2_request *head;
+
+ unsigned int refcount;
+};
+
+static struct db_oauth2 *db_oauth2_head = NULL;
+
+#undef DEF_STR
+#undef DEF_BOOL
+#undef DEF_INT
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, passdb_oauth2_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, passdb_oauth2_settings)
+#define DEF_INT(name) DEF_STRUCT_INT(name, passdb_oauth2_settings)
+
+static struct setting_def setting_defs[] = {
+ DEF_STR(tokeninfo_url),
+ DEF_STR(grant_url),
+ DEF_STR(introspection_url),
+ DEF_STR(scope),
+ DEF_BOOL(force_introspection),
+ DEF_STR(introspection_mode),
+ DEF_STR(username_format),
+ DEF_STR(username_attribute),
+ DEF_STR(pass_attrs),
+ DEF_STR(local_validation_key_dict),
+ DEF_STR(active_attribute),
+ DEF_STR(active_value),
+ DEF_STR(client_id),
+ DEF_STR(client_secret),
+ DEF_STR(issuers),
+ DEF_STR(openid_configuration_url),
+ DEF_INT(timeout_msecs),
+ DEF_INT(max_idle_time_msecs),
+ DEF_INT(max_parallel_connections),
+ DEF_INT(max_pipelined_requests),
+ DEF_BOOL(send_auth_headers),
+ DEF_BOOL(use_grant_password),
+
+ DEF_STR(tls_ca_cert_file),
+ DEF_STR(tls_ca_cert_dir),
+ DEF_STR(tls_cert_file),
+ DEF_STR(tls_key_file),
+ DEF_STR(tls_cipher_suite),
+ DEF_BOOL(tls_allow_invalid_cert),
+
+ DEF_STR(rawlog_dir),
+
+ DEF_BOOL(debug),
+
+ { 0, NULL, 0 }
+};
+
+static struct passdb_oauth2_settings default_oauth2_settings = {
+ .tokeninfo_url = "",
+ .grant_url = "",
+ .introspection_url = "",
+ .scope = "",
+ .force_introspection = FALSE,
+ .introspection_mode = "",
+ .username_format = "%Lu",
+ .username_attribute = "email",
+ .active_attribute = "",
+ .active_value = "",
+ .client_id = "",
+ .client_secret = "",
+ .issuers = "",
+ .openid_configuration_url = "",
+ .pass_attrs = "",
+ .local_validation_key_dict = "",
+ .rawlog_dir = "",
+ .timeout_msecs = 0,
+ .max_idle_time_msecs = 60000,
+ .max_parallel_connections = 10,
+ .max_pipelined_requests = 1,
+ .tls_ca_cert_file = NULL,
+ .tls_ca_cert_dir = NULL,
+ .tls_cert_file = NULL,
+ .tls_key_file = NULL,
+ .tls_cipher_suite = "HIGH:!SSLv2",
+ .tls_allow_invalid_cert = FALSE,
+ .send_auth_headers = FALSE,
+ .use_grant_password = FALSE,
+ .debug = FALSE,
+};
+
+static const char *parse_setting(const char *key, const char *value,
+ struct db_oauth2 *db)
+{
+ return parse_setting_from_defs(db->pool, setting_defs,
+ &db->set, key, value);
+}
+
+struct db_oauth2 *db_oauth2_init(const char *config_path)
+{
+ struct db_oauth2 *db;
+ const char *error;
+ struct ssl_iostream_settings ssl_set;
+ struct http_client_settings http_set;
+
+ for(db = db_oauth2_head; db != NULL; db = db->next) {
+ if (strcmp(db->config_path, config_path) == 0) {
+ db->refcount++;
+ return db;
+ }
+ }
+
+ pool_t pool = pool_alloconly_create("db_oauth2", 128);
+ db = p_new(pool, struct db_oauth2, 1);
+ db->pool = pool;
+ db->refcount = 1;
+ db->config_path = p_strdup(pool, config_path);
+ db->set = default_oauth2_settings;
+
+ if (!settings_read_nosection(config_path, parse_setting, db, &error))
+ i_fatal("oauth2 %s: %s", config_path, error);
+
+ db->tmpl = passdb_template_build(pool, db->set.pass_attrs);
+
+ i_zero(&ssl_set);
+ i_zero(&http_set);
+
+ ssl_set.cipher_list = db->set.tls_cipher_suite;
+ ssl_set.ca_file = db->set.tls_ca_cert_file;
+ ssl_set.ca_dir = db->set.tls_ca_cert_dir;
+ if (db->set.tls_cert_file != NULL && *db->set.tls_cert_file != '\0') {
+ ssl_set.cert.cert = db->set.tls_cert_file;
+ ssl_set.cert.key = db->set.tls_key_file;
+ }
+ ssl_set.prefer_server_ciphers = TRUE;
+ ssl_set.allow_invalid_cert = db->set.tls_allow_invalid_cert;
+ ssl_set.verbose = db->set.debug;
+ ssl_set.verbose_invalid_cert = db->set.debug;
+ http_set.ssl = &ssl_set;
+
+ http_set.dns_client_socket_path = "dns-client";
+ http_set.user_agent = "dovecot-oauth2-passdb/" DOVECOT_VERSION;
+
+ if (*db->set.local_validation_key_dict == '\0' &&
+ *db->set.tokeninfo_url == '\0' &&
+ (*db->set.grant_url == '\0' || *db->set.client_id == '\0') &&
+ *db->set.introspection_url == '\0')
+ i_fatal("oauth2: Password grant, tokeninfo, introspection URL or "
+ "validation key dictionary must be given");
+
+ if (*db->set.rawlog_dir != '\0')
+ http_set.rawlog_dir = db->set.rawlog_dir;
+
+ http_set.max_idle_time_msecs = db->set.max_idle_time_msecs;
+ http_set.max_parallel_connections = db->set.max_parallel_connections;
+ http_set.max_pipelined_requests = db->set.max_pipelined_requests;
+ http_set.no_auto_redirect = FALSE;
+ http_set.no_auto_retry = TRUE;
+ http_set.debug = db->set.debug;
+ http_set.event_parent = auth_event;
+
+ db->client = http_client_init(&http_set);
+
+ i_zero(&db->oauth2_set);
+ db->oauth2_set.client = db->client;
+ db->oauth2_set.tokeninfo_url = db->set.tokeninfo_url,
+ db->oauth2_set.grant_url = db->set.grant_url,
+ db->oauth2_set.introspection_url = db->set.introspection_url;
+ db->oauth2_set.client_id = db->set.client_id;
+ db->oauth2_set.client_secret = db->set.client_secret;
+ db->oauth2_set.timeout_msecs = db->set.timeout_msecs;
+ db->oauth2_set.send_auth_headers = db->set.send_auth_headers;
+ db->oauth2_set.use_grant_password = db->set.use_grant_password;
+ db->oauth2_set.scope = db->set.scope;
+
+ if (*db->set.active_attribute == '\0' &&
+ *db->set.active_value != '\0')
+ i_fatal("oauth2: Cannot have empty active_attribute is active_value is set");
+
+ if (*db->set.introspection_mode == '\0' ||
+ strcmp(db->set.introspection_mode, "auth") == 0) {
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_GET_AUTH;
+ } else if (strcmp(db->set.introspection_mode, "get") == 0) {
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_GET;
+ } else if (strcmp(db->set.introspection_mode, "post") == 0) {
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_POST;
+ } else if (strcmp(db->set.introspection_mode, "local") == 0) {
+ if (*db->set.local_validation_key_dict == '\0')
+ i_fatal("oauth2: local_validation_key_dict is required "
+ "for local introspection.");
+ db->oauth2_set.introspection_mode = INTROSPECTION_MODE_LOCAL;
+ } else {
+ i_fatal("oauth2: Invalid value '%s' for introspection mode, must be on auth, get, post or local",
+ db->set.introspection_mode);
+ }
+
+ if (db->oauth2_set.introspection_mode == INTROSPECTION_MODE_LOCAL) {
+ struct dict_settings dict_set = {
+ .base_dir = global_auth_settings->base_dir,
+ .event_parent = auth_event,
+ };
+ if (dict_init(db->set.local_validation_key_dict, &dict_set,
+ &db->oauth2_set.key_dict, &error) < 0)
+ i_fatal("Cannot initialize key dict: %s", error);
+ /* failure to initialize dcrypt is not fatal - we can still
+ validate HMAC based keys */
+ (void)dcrypt_initialize(NULL, NULL, NULL);
+ /* initialize key cache */
+ db->oauth2_set.key_cache = oauth2_validation_key_cache_init();
+ }
+
+ if (*db->set.issuers != '\0')
+ db->oauth2_set.issuers = (const char *const *)
+ p_strsplit_spaces(pool, db->set.issuers, " ");
+
+ if (*db->set.openid_configuration_url != '\0') {
+ struct http_url *parsed_url ATTR_UNUSED;
+ if (http_url_parse(db->set.openid_configuration_url, NULL, 0,
+ pool_datastack_create(), &parsed_url,
+ &error) < 0) {
+ i_fatal("Invalid openid_configuration_url: %s",
+ error);
+ }
+ }
+
+ DLLIST_PREPEND(&db_oauth2_head, db);
+
+ return db;
+}
+
+void db_oauth2_ref(struct db_oauth2 *db)
+{
+ i_assert(db->refcount > 0);
+ db->refcount++;
+}
+
+void db_oauth2_unref(struct db_oauth2 **_db)
+{
+ struct db_oauth2 *ptr, *db = *_db;
+ i_assert(db->refcount > 0);
+
+ if (--db->refcount > 0) return;
+
+ for(ptr = db_oauth2_head; ptr != NULL; ptr = ptr->next) {
+ if (ptr == db) {
+ DLLIST_REMOVE(&db_oauth2_head, ptr);
+ break;
+ }
+ }
+
+ i_assert(ptr != NULL && ptr == db);
+
+ /* make sure all requests are aborted */
+ while (db->head != NULL)
+ oauth2_request_abort(&db->head->req);
+
+ http_client_deinit(&db->client);
+ if (db->oauth2_set.key_dict != NULL)
+ dict_deinit(&db->oauth2_set.key_dict);
+ oauth2_validation_key_cache_deinit(&db->oauth2_set.key_cache);
+ pool_unref(&db->pool);
+}
+
+static void
+db_oauth2_add_openid_config_url(struct db_oauth2_request *req)
+{
+ /* 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 moved to mech-oauth2 once the validation
+ result et al is handled there.
+ */
+ req->auth_request->openid_config_url =
+ p_strdup_empty(req->auth_request->pool,
+ req->db->set.openid_configuration_url);
+}
+
+const char *db_oauth2_get_openid_configuration_url(const struct db_oauth2 *db)
+{
+ return db->set.openid_configuration_url;
+}
+
+static bool
+db_oauth2_have_all_fields(struct db_oauth2_request *req)
+{
+ unsigned int n,i;
+ unsigned int size,idx;
+ const char *const *args = passdb_template_get_args(req->db->tmpl, &n);
+
+ if (req->fields == NULL)
+ return FALSE;
+
+ for(i=1;i<n;i+=2) {
+ const char *ptr = args[i];
+ while(ptr != NULL) {
+ ptr = strchr(ptr, '%');
+ if (ptr != NULL) {
+ const char *field;
+ ptr++;
+ var_get_key_range(ptr, &idx, &size);
+ ptr = ptr+idx;
+ field = t_strndup(ptr,size);
+ if (str_begins(field, "oauth2:") &&
+ !auth_fields_exists(req->fields, field+7))
+ return FALSE;
+ ptr = ptr+size;
+ }
+ }
+ }
+
+ if (!auth_fields_exists(req->fields, req->db->set.username_attribute))
+ return FALSE;
+ if (*req->db->set.active_attribute != '\0' && !auth_fields_exists(req->fields, req->db->set.active_attribute))
+ return FALSE;
+
+ return TRUE;
+}
+
+static const char *field_get_default(const char *data)
+{
+ const char *p;
+
+ p = strchr(data, ':');
+ if (p == NULL)
+ return "";
+ else {
+ /* default value given */
+ return p+1;
+ }
+}
+
+static int db_oauth2_var_expand_func_oauth2(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct db_oauth2_request *ctx = context;
+ const char *field_name = t_strcut(data, ':');
+ const char *value = NULL;
+
+ if (ctx->fields != NULL)
+ value = auth_fields_find(ctx->fields, field_name);
+ *value_r = value != NULL ? value : field_get_default(data);
+
+ return 1;
+}
+
+static const char *escape_none(const char *value, const struct auth_request *req ATTR_UNUSED)
+{
+ return value;
+}
+
+static const struct var_expand_table *
+db_oauth2_value_get_var_expand_table(struct auth_request *auth_request,
+ const char *oauth2_value)
+{
+ struct var_expand_table *table;
+ unsigned int count = 1;
+
+ table = auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, NULL, &count);
+ table[0].key = '$';
+ table[0].value = oauth2_value;
+ return table;
+}
+
+static bool
+db_oauth2_template_export(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ /* var=$ expands into var=${oauth2:var} */
+ const struct var_expand_func_table funcs_table[] = {
+ { "oauth2", db_oauth2_var_expand_func_oauth2 },
+ { NULL, NULL }
+ };
+ string_t *dest;
+ const char *const *args, *value, *error;
+ struct passdb_template *tmpl = req->db->tmpl;
+ unsigned int i, count;
+
+ if (passdb_template_is_empty(tmpl))
+ return TRUE;
+
+ dest = t_str_new(256);
+ args = passdb_template_get_args(tmpl, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (args[i+1] == NULL)
+ value = "";
+ else {
+ str_truncate(dest, 0);
+ const struct var_expand_table *
+ table = db_oauth2_value_get_var_expand_table(req->auth_request,
+ auth_fields_find(req->fields, args[i]));
+ if (var_expand_with_funcs(dest, args[i+1], table, funcs_table,
+ req, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "var_expand(%s) failed: %s",
+ args[i+1], error);
+ *result_r = PASSDB_RESULT_INTERNAL_FAILURE;
+ return FALSE;
+ }
+ value = str_c(dest);
+ }
+
+ auth_request_set_field(req->auth_request, args[i], value,
+ STATIC_PASS_SCHEME);
+ }
+ return TRUE;
+}
+
+static void db_oauth2_fields_merge(struct db_oauth2_request *req,
+ ARRAY_TYPE(oauth2_field) *fields)
+{
+ const struct oauth2_field *field;
+
+ if (req->fields == NULL)
+ req->fields = auth_fields_init(req->pool);
+
+ array_foreach(fields, field) {
+ e_debug(authdb_event(req->auth_request),
+ "Processing field %s",
+ field->name);
+ auth_fields_add(req->fields, field->name, field->value, 0);
+ }
+}
+
+static const char *
+db_oauth2_field_find(const ARRAY_TYPE(oauth2_field) *fields, const char *name)
+{
+ const struct oauth2_field *f;
+
+ array_foreach(fields, f) {
+ if (strcmp(f->name, name) == 0)
+ return f->value;
+ }
+ return NULL;
+}
+
+static void db_oauth2_callback(struct db_oauth2_request *req,
+ enum passdb_result result,
+ const char *error_prefix, const char *error)
+{
+ db_oauth2_lookup_callback_t *callback = req->callback;
+ req->callback = NULL;
+
+ i_assert(result == PASSDB_RESULT_OK || error != NULL);
+
+ if (result != PASSDB_RESULT_OK)
+ db_oauth2_add_openid_config_url(req);
+
+ /* Successful lookups were logged by the caller. Failed lookups will be
+ logged either with e_error() or e_info() by the callback. */
+ if (callback != NULL) {
+ DLLIST_REMOVE(&req->db->head, req);
+ if (result != PASSDB_RESULT_OK)
+ error = t_strconcat(error_prefix, error, NULL);
+ callback(req, result, error, req->context);
+ }
+}
+
+static bool
+db_oauth2_validate_username(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ const char *error;
+ struct var_expand_table table[] = {
+ { 'u', NULL, "user" },
+ { 'n', NULL, "username" },
+ { 'd', NULL, "domain" },
+ { '\0', NULL, NULL }
+ };
+ const char *username_value =
+ auth_fields_find(req->fields, req->db->set.username_attribute);
+
+ if (username_value == NULL) {
+ *result_r = PASSDB_RESULT_INTERNAL_FAILURE;
+ *error_r = "No username returned";
+ return FALSE;
+ }
+
+ table[0].value = username_value;
+ table[1].value = t_strcut(username_value, '@');
+ table[2].value = i_strchr_to_next(username_value, '@');
+
+ string_t *username_req = t_str_new(32);
+ string_t *username_val = t_str_new(strlen(username_value));
+
+ if (auth_request_var_expand(username_req, req->db->set.username_format, req->auth_request, escape_none, &error) < 0 ||
+ var_expand(username_val, req->db->set.username_format, table, &error) < 0) {
+ *error_r = t_strdup_printf("var_expand(%s) failed: %s",
+ req->db->set.username_format, error);
+ *result_r = PASSDB_RESULT_INTERNAL_FAILURE;
+ return FALSE;
+ } else if (!str_equals(username_req, username_val)) {
+ *error_r = t_strdup_printf("Username '%s' did not match '%s'",
+ str_c(username_req), str_c(username_val));
+ *result_r = PASSDB_RESULT_USER_UNKNOWN;
+ return FALSE;
+ } else {
+ req->username = p_strdup(req->pool, str_c(username_val));
+ return TRUE;
+ }
+}
+
+static bool
+db_oauth2_user_is_enabled(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ if (*req->db->set.active_attribute == '\0' ) {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute is not configured; skipping the check");
+ return TRUE;
+ }
+
+ const char *active_value =
+ auth_fields_find(req->fields, req->db->set.active_attribute);
+
+ if (active_value == NULL) {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute \"%s\" is not present in the oauth2 server's response",
+ req->db->set.active_attribute);
+ *error_r = "Missing active_attribute from token";
+ *result_r = PASSDB_RESULT_PASSWORD_MISMATCH;
+ return FALSE;
+ }
+
+ if (*req->db->set.active_value == '\0') {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute \"%s\" present; skipping the check on value",
+ req->db->set.active_attribute);
+ return TRUE;
+ }
+
+ if (strcmp(req->db->set.active_value, active_value) != 0) {
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute check failed: expected %s=\"%s\" but got \"%s\"",
+ req->db->set.active_attribute,
+ req->db->set.active_value,
+ active_value);
+ *error_r = "Provided token is not valid";
+ *result_r = PASSDB_RESULT_PASSWORD_MISMATCH;
+ return FALSE;
+ }
+
+ e_debug(authdb_event(req->auth_request),
+ "oauth2 active_attribute check succeeded");
+ return TRUE;
+}
+
+static bool
+db_oauth2_token_in_scope(struct db_oauth2_request *req,
+ enum passdb_result *result_r, const char **error_r)
+{
+ if (*req->db->set.scope != '\0') {
+ bool found = FALSE;
+ const char *value = auth_fields_find(req->fields, "scope");
+ if (value == NULL)
+ value = auth_fields_find(req->fields, "aud");
+ e_debug(authdb_event(req->auth_request),
+ "Token scope(s): %s",
+ value);
+ if (value != NULL) {
+ const char **wanted_scopes =
+ t_strsplit_spaces(req->db->set.scope, " ");
+ const char **scopes = t_strsplit_spaces(value, " ");
+ for (; !found && *wanted_scopes != NULL; wanted_scopes++)
+ found = str_array_find(scopes, *wanted_scopes);
+ }
+ if (!found) {
+ *error_r = t_strdup_printf("Token is not valid for scope '%s'",
+ req->db->set.scope);
+ *result_r = PASSDB_RESULT_USER_DISABLED;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void db_oauth2_process_fields(struct db_oauth2_request *req,
+ enum passdb_result *result_r,
+ const char **error_r)
+{
+ *error_r = NULL;
+
+ if (db_oauth2_user_is_enabled(req, result_r, error_r) &&
+ db_oauth2_validate_username(req, result_r, error_r) &&
+ db_oauth2_token_in_scope(req, result_r, error_r) &&
+ db_oauth2_template_export(req, result_r, error_r)) {
+ *result_r = PASSDB_RESULT_OK;
+ } else {
+ i_assert(*result_r != PASSDB_RESULT_OK && *error_r != NULL);
+ }
+}
+
+static void
+db_oauth2_introspect_continue(struct oauth2_request_result *result,
+ struct db_oauth2_request *req)
+{
+ enum passdb_result passdb_result;
+ const char *error;
+
+ req->req = NULL;
+
+ if (result->error != NULL) {
+ /* fail here */
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ error = result->error;
+ } else {
+ e_debug(authdb_event(req->auth_request),
+ "Introspection succeeded");
+ db_oauth2_fields_merge(req, result->fields);
+ db_oauth2_process_fields(req, &passdb_result, &error);
+ }
+ db_oauth2_callback(req, passdb_result, "Introspection failed: ", error);
+}
+
+static void db_oauth2_lookup_introspect(struct db_oauth2_request *req)
+{
+ struct oauth2_request_input input;
+ i_zero(&input);
+
+ e_debug(authdb_event(req->auth_request),
+ "Making introspection request to %s",
+ req->db->set.introspection_url);
+ input.token = req->token;
+ input.local_ip = req->auth_request->fields.local_ip;
+ input.local_port = req->auth_request->fields.local_port;
+ input.remote_ip = req->auth_request->fields.remote_ip;
+ input.remote_port = req->auth_request->fields.remote_port;
+ input.real_local_ip = req->auth_request->fields.real_local_ip;
+ input.real_local_port = req->auth_request->fields.real_local_port;
+ input.real_remote_ip = req->auth_request->fields.real_remote_ip;
+ input.real_remote_port = req->auth_request->fields.real_remote_port;
+ input.service = req->auth_request->fields.service;
+
+ req->req = oauth2_introspection_start(&req->db->oauth2_set, &input,
+ db_oauth2_introspect_continue, req);
+}
+
+static void db_oauth2_local_validation(struct db_oauth2_request *req,
+ const char *token)
+{
+ bool is_jwt ATTR_UNUSED;
+ const char *error = NULL;
+ enum passdb_result passdb_result;
+ ARRAY_TYPE(oauth2_field) fields;
+ t_array_init(&fields, 8);
+ if (oauth2_try_parse_jwt(&req->db->oauth2_set, token,
+ &fields, &is_jwt, &error) < 0) {
+ passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ } else {
+ db_oauth2_fields_merge(req, &fields);
+ db_oauth2_process_fields(req, &passdb_result, &error);
+ }
+ if (passdb_result == PASSDB_RESULT_OK) {
+ e_debug(authdb_event(req->auth_request),
+ "Local validation succeeded");
+ }
+ db_oauth2_callback(req, passdb_result,
+ "Local validation failed: ", error);
+}
+
+static void
+db_oauth2_lookup_continue_valid(struct db_oauth2_request *req,
+ ARRAY_TYPE(oauth2_field) *fields,
+ const char *error_prefix)
+{
+ enum passdb_result passdb_result;
+ const char *error;
+
+ db_oauth2_fields_merge(req, fields);
+ if (db_oauth2_have_all_fields(req) &&
+ !req->db->set.force_introspection) {
+ /* pass */
+ } else if (req->db->oauth2_set.introspection_mode ==
+ INTROSPECTION_MODE_LOCAL) {
+ e_debug(authdb_event(req->auth_request),
+ "Attempting to locally validate token");
+ db_oauth2_local_validation(req, req->token);
+ return;
+ } else if (!db_oauth2_user_is_enabled(req, &passdb_result, &error)) {
+ db_oauth2_callback(req, passdb_result,
+ "Token is not valid: ", error);
+ return;
+ } else if (*req->db->set.introspection_url != '\0') {
+ db_oauth2_lookup_introspect(req);
+ return;
+ }
+ db_oauth2_process_fields(req, &passdb_result, &error);
+ db_oauth2_callback(req, passdb_result, error_prefix, error);
+}
+
+static void
+db_oauth2_lookup_continue(struct oauth2_request_result *result,
+ struct db_oauth2_request *req)
+{
+ i_assert(req->token != NULL);
+ req->req = NULL;
+
+ if (result->error != NULL) {
+ db_oauth2_callback(req, PASSDB_RESULT_INTERNAL_FAILURE,
+ "Token validation failed: ", result->error);
+ } else if (!result->valid) {
+ db_oauth2_callback(req, PASSDB_RESULT_PASSWORD_MISMATCH,
+ "Token validation failed: ",
+ "Invalid token");
+ } else {
+ e_debug(authdb_event(req->auth_request),
+ "Token validation succeeded");
+ db_oauth2_lookup_continue_valid(req, result->fields,
+ "Token validation failed: ");
+ }
+}
+
+static void
+db_oauth2_lookup_passwd_grant(struct oauth2_request_result *result,
+ struct db_oauth2_request *req)
+{
+ enum passdb_result passdb_result;
+ const char *token, *error;
+
+ i_assert(req->token == NULL);
+ req->req = NULL;
+
+ if (result->valid) {
+ e_debug(authdb_event(req->auth_request),
+ "Password grant succeeded");
+ token = db_oauth2_field_find(result->fields, "access_token");
+ if (token == NULL) {
+ db_oauth2_callback(req, PASSDB_RESULT_INTERNAL_FAILURE,
+ "Password grant failed: ",
+ "OAuth2 token missing from reply");
+ } else {
+ req->token = p_strdup(req->pool, token);
+ db_oauth2_lookup_continue_valid(req, result->fields,
+ "Password grant failed: ");
+ }
+ return;
+ }
+
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ if (result->error != NULL)
+ error = result->error;
+ else {
+ passdb_result = PASSDB_RESULT_INTERNAL_FAILURE;
+ error = db_oauth2_field_find(result->fields, "error");
+ if (error == NULL)
+ error = "OAuth2 server returned failure without error field";
+ else if (strcmp("invalid_grant", error) == 0)
+ passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH;
+ }
+ db_oauth2_callback(req, passdb_result,
+ "Password grant failed: ", error);
+}
+
+#undef db_oauth2_lookup
+void db_oauth2_lookup(struct db_oauth2 *db, struct db_oauth2_request *req,
+ const char *token, struct auth_request *request,
+ db_oauth2_lookup_callback_t *callback, void *context)
+{
+ struct oauth2_request_input input;
+ i_zero(&input);
+
+ req->db = db;
+ req->token = p_strdup(req->pool, token);
+ req->callback = callback;
+ req->context = context;
+ req->auth_request = request;
+
+ input.token = token;
+ input.local_ip = req->auth_request->fields.local_ip;
+ input.local_port = req->auth_request->fields.local_port;
+ input.remote_ip = req->auth_request->fields.remote_ip;
+ input.remote_port = req->auth_request->fields.remote_port;
+ input.real_local_ip = req->auth_request->fields.real_local_ip;
+ input.real_local_port = req->auth_request->fields.real_local_port;
+ input.real_remote_ip = req->auth_request->fields.real_remote_ip;
+ input.real_remote_port = req->auth_request->fields.real_remote_port;
+ input.service = req->auth_request->fields.service;
+
+ if (db->oauth2_set.introspection_mode == INTROSPECTION_MODE_LOCAL &&
+ !db_oauth2_uses_password_grant(db)) {
+ /* try to validate token locally */
+ e_debug(authdb_event(req->auth_request),
+ "Attempting to locally validate token");
+ db_oauth2_local_validation(req, request->mech_password);
+ return;
+
+ }
+ if (db->oauth2_set.use_grant_password) {
+ e_debug(authdb_event(req->auth_request),
+ "Making grant url request to %s",
+ db->set.grant_url);
+ /* There is no valid token until grant looks it up. */
+ req->token = NULL;
+ req->req = oauth2_passwd_grant_start(&db->oauth2_set, &input,
+ request->fields.user, request->mech_password,
+ db_oauth2_lookup_passwd_grant, req);
+ } else if (*db->oauth2_set.tokeninfo_url == '\0') {
+ e_debug(authdb_event(req->auth_request),
+ "Making introspection request to %s",
+ db->set.introspection_url);
+ req->req = oauth2_introspection_start(&req->db->oauth2_set, &input,
+ db_oauth2_introspect_continue, req);
+ } else {
+ e_debug(authdb_event(req->auth_request),
+ "Making token validation lookup to %s",
+ db->oauth2_set.tokeninfo_url);
+ req->req = oauth2_token_validation_start(&db->oauth2_set, &input,
+ db_oauth2_lookup_continue, req);
+ }
+ i_assert(req->req != NULL);
+ DLLIST_PREPEND(&db->head, req);
+}
+
+bool db_oauth2_uses_password_grant(const struct db_oauth2 *db)
+{
+ return db->set.use_grant_password;
+}