]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-oauth2: Add support library for OAUTH2
authorAki Tuomi <aki.tuomi@dovecot.fi>
Thu, 26 Jan 2017 08:49:08 +0000 (10:49 +0200)
committerGitLab <gitlab@git.dovecot.net>
Thu, 16 Feb 2017 13:43:42 +0000 (15:43 +0200)
configure.ac
src/Makefile.am
src/lib-oauth2/Makefile.am [new file with mode: 0644]
src/lib-oauth2/oauth2-introspect.c [new file with mode: 0644]
src/lib-oauth2/oauth2-private.h [new file with mode: 0644]
src/lib-oauth2/oauth2-refresh.c [new file with mode: 0644]
src/lib-oauth2/oauth2-token-validate.c [new file with mode: 0644]
src/lib-oauth2/oauth2.c [new file with mode: 0644]
src/lib-oauth2/oauth2.h [new file with mode: 0644]

index 88c76451ed667184e326a759d71067079000f5d5..99cd7dd29dda8e9b96e31a27afa0e648deaae8c6 100644 (file)
@@ -665,7 +665,7 @@ dnl **
 dnl ** Shared libraries usage
 dnl **
 
-LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-program-client/libprogram_client.la $(top_builddir)/src/lib-master/libmaster.la $(top_builddir)/src/lib-settings/libsettings.la $(top_builddir)/src/lib-stats/libstats.la $(top_builddir)/src/lib-http/libhttp.la $(top_builddir)/src/lib-fs/libfs.la $(top_builddir)/src/lib-dict/libdict.la $(top_builddir)/src/lib-smtp/libsmtp.la $(top_builddir)/src/lib-dns/libdns.la $(top_builddir)/src/lib-imap/libimap.la $(top_builddir)/src/lib-mail/libmail.la $(top_builddir)/src/lib-sasl/libsasl.la $(top_builddir)/src/lib-auth/libauth.la $(top_builddir)/src/lib-charset/libcharset.la $(top_builddir)/src/lib-ssl-iostream/libssl_iostream.la $(top_builddir)/src/lib-dcrypt/libdcrypt.la $(top_builddir)/src/lib-test/libtest.la $(top_builddir)/src/lib/liblib.la'
+LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-oauth2/liboauth2.la $(top_builddir)/src/lib-program-client/libprogram_client.la $(top_builddir)/src/lib-master/libmaster.la $(top_builddir)/src/lib-settings/libsettings.la $(top_builddir)/src/lib-stats/libstats.la $(top_builddir)/src/lib-http/libhttp.la $(top_builddir)/src/lib-fs/libfs.la $(top_builddir)/src/lib-dict/libdict.la $(top_builddir)/src/lib-smtp/libsmtp.la $(top_builddir)/src/lib-dns/libdns.la $(top_builddir)/src/lib-imap/libimap.la $(top_builddir)/src/lib-mail/libmail.la $(top_builddir)/src/lib-sasl/libsasl.la $(top_builddir)/src/lib-auth/libauth.la $(top_builddir)/src/lib-charset/libcharset.la $(top_builddir)/src/lib-ssl-iostream/libssl_iostream.la $(top_builddir)/src/lib-dcrypt/libdcrypt.la $(top_builddir)/src/lib-test/libtest.la $(top_builddir)/src/lib/liblib.la'
 if test "$want_shared_libs" = "yes"; then
   LIBDOVECOT_DEPS='$(top_builddir)/src/lib-dovecot/libdovecot.la'
   LIBDOVECOT="$LIBDOVECOT_DEPS \$(MODULE_LIBS)"
@@ -853,6 +853,7 @@ src/lib-dns/Makefile
 src/lib-fs/Makefile
 src/lib-fts/Makefile
 src/lib-http/Makefile
+src/lib-oauth2/Makefile
 src/lib-imap/Makefile
 src/lib-imap-storage/Makefile
 src/lib-imap-client/Makefile
index 53b5f1d99be09aba3084dede49f0248326cd2bb0..77735ff67300784290062ef287c9b6a04a20f4a5 100644 (file)
@@ -21,7 +21,8 @@ LIBDOVECOT_SUBDIRS = \
        lib-smtp \
        lib-imap \
        lib-imap-storage \
-       lib-program-client
+       lib-program-client \
+       lib-oauth2
 
 SUBDIRS = \
        $(LIBDOVECOT_SUBDIRS) \
diff --git a/src/lib-oauth2/Makefile.am b/src/lib-oauth2/Makefile.am
new file mode 100644 (file)
index 0000000..65250cf
--- /dev/null
@@ -0,0 +1,23 @@
+AM_CPPFLAGS = \
+       -I$(top_srcdir)/src/lib \
+       -I$(top_srcdir)/src/lib-http \
+       -I$(top_srcdir)/src/lib-settings
+
+noinst_LTLIBRARIES=liboauth2.la
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+       oauth2.h
+
+noinst_HEADERS = \
+       oauth2-private.h
+
+liboauth2_la_SOURCES = \
+       oauth2.c \
+       oauth2-token-validate.c \
+       oauth2-introspect.c \
+       oauth2-refresh.c
+
+check_programs = \
+       oauth2-server \
+       test-oauth2
diff --git a/src/lib-oauth2/oauth2-introspect.c b/src/lib-oauth2/oauth2-introspect.c
new file mode 100644 (file)
index 0000000..ce9d2a8
--- /dev/null
@@ -0,0 +1,125 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "json-parser.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+
+static void
+oauth2_introspection_callback(struct oauth2_request *req,
+                             struct oauth2_introspection_result *res)
+{
+       i_assert(res->success == (res->error == NULL));
+       i_assert(req->is_callback != NULL);
+       oauth2_introspection_callback_t *callback = req->is_callback;
+       req->is_callback = NULL;
+       callback(res, req->is_context);
+       oauth2_request_free_internal(req);
+}
+
+static void
+oauth2_introspect_continue(struct oauth2_request *req, bool success,
+                          const char *error)
+{
+       struct oauth2_introspection_result res;
+       i_zero(&res);
+
+       res.success = success;
+       res.error = error;
+       res.fields = &req->fields;
+
+       oauth2_introspection_callback(req, &res);
+}
+
+static void
+oauth2_introspect_response(const struct http_response *response,
+                          struct oauth2_request *req)
+{
+       if (response->status / 100 != 2) {
+               oauth2_introspect_continue(req, FALSE, response->reason);
+       } else {
+               if (response->payload == NULL) {
+                       oauth2_introspect_continue(req, FALSE, "Missing response body");
+                       return;
+               }
+               req->is = response->payload;
+               i_stream_ref(req->is);
+               req->parser = json_parser_init(req->is);
+               req->json_parsed_cb = oauth2_introspect_continue;
+               req->io = io_add_istream(req->is, oauth2_parse_json, req);
+               oauth2_parse_json(req);
+       }
+}
+
+#undef oauth2_introspection_start
+struct oauth2_request*
+oauth2_introspection_start(const struct oauth2_settings *set,
+                          const struct oauth2_request_input *input,
+                          oauth2_introspection_callback_t *callback,
+                          void *context)
+{
+       i_assert(oauth2_valid_token(input->token));
+
+       pool_t pool = pool_alloconly_create_clean("oauth2 introspection", 1024);
+       struct oauth2_request *req =
+               p_new(pool, struct oauth2_request, 1);
+       struct oauth2_introspection_result fail = {
+               .success = FALSE,
+       };
+       struct http_url *url;
+       const char *error;
+
+       req->pool = pool;
+       req->set = set;
+       req->is_callback = callback;
+       req->is_context = context;
+
+       string_t *enc = t_str_new(64);
+       str_append(enc, req->set->introspection_url);
+
+       if (set->introspection_mode == INTROSPECTION_MODE_GET) {
+               http_url_escape_param(enc, input->token);
+       }
+
+       if (http_url_parse(str_c(enc), NULL, 0, pool, &url, &error) < 0) {
+               fail.error = t_strdup_printf("http_url_parse(%s) failed: %s",
+                                            str_c(enc), error);
+               oauth2_introspection_callback(req, &fail);
+               return req;
+       }
+
+       if (set->introspection_mode == INTROSPECTION_MODE_POST) {
+               req->req = http_client_request_url(req->set->client, "POST", url,
+                                                  oauth2_introspect_response,
+                                                  req);
+               /* add token */
+               enc = t_str_new(strlen(input->token)+6);
+               str_append(enc, "token=");
+               http_url_escape_param(enc, input->token);
+               http_client_request_set_payload_data(req->req, enc->data, enc->used);
+       } else {
+               req->req = http_client_request_url(req->set->client, "GET", url,
+                                                  oauth2_introspect_response,
+                                                  req);
+       }
+
+       if (set->introspection_mode == INTROSPECTION_MODE_GET_AUTH)
+               http_client_request_add_header(req->req,
+                                              "Authorization",
+                                              t_strdup_printf("Bearer %s",
+                                                              input->token));
+
+       oauth2_request_set_headers(req, input);
+
+       http_client_request_set_timeout_msecs(req->req,
+                                             req->set->timeout_msecs);
+       http_client_request_submit(req->req);
+
+       return req;
+}
+
diff --git a/src/lib-oauth2/oauth2-private.h b/src/lib-oauth2/oauth2-private.h
new file mode 100644 (file)
index 0000000..c5d8958
--- /dev/null
@@ -0,0 +1,42 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+#ifndef OAUTH2_PRIVATE_H
+#define OAUTH2_PRIVATE_H 1
+
+struct oauth2_request {
+       pool_t pool;
+
+       const struct oauth2_settings *set;
+       struct http_client_request *req;
+       struct json_parser *parser;
+       struct istream *is;
+       struct io *io;
+
+       const char *username;
+
+       void (*json_parsed_cb)(struct oauth2_request*, bool success,
+                              const char *error);
+
+       ARRAY_TYPE(oauth2_field) fields;
+       char *field_name;
+
+       oauth2_token_validation_callback_t *tv_callback;
+       void *tv_context;
+
+       oauth2_introspection_callback_t *is_callback;
+       void *is_context;
+
+       oauth2_refresh_callback_t *re_callback;
+       void *re_context;
+
+       /* indicates whether token is valid */
+       bool valid:1;
+};
+
+void oauth2_request_set_headers(struct oauth2_request *req,
+                               const struct oauth2_request_input *input);
+
+void oauth2_request_free_internal(struct oauth2_request *req);
+
+void oauth2_parse_json(struct oauth2_request *req);
+
+#endif
diff --git a/src/lib-oauth2/oauth2-refresh.c b/src/lib-oauth2/oauth2-refresh.c
new file mode 100644 (file)
index 0000000..00ea604
--- /dev/null
@@ -0,0 +1,156 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "str.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "json-parser.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+
+static void
+oauth2_refresh_callback(struct oauth2_request *req,
+                       struct oauth2_refresh_result *res)
+{
+       i_assert(res->success == (res->error == NULL));
+       i_assert(req->re_callback != NULL);
+       oauth2_refresh_callback_t *callback = req->re_callback;
+       req->re_callback = NULL;
+       callback(res, req->re_context);
+       oauth2_request_free_internal(req);
+}
+
+static bool
+oauth2_refresh_field_parse(const struct oauth2_field *field,
+                          struct oauth2_refresh_result *res)
+{
+       if (strcasecmp(field->name, "expires_in") == 0) {
+               uint32_t expires_in = 0;
+               if (str_to_uint32(field->value, &expires_in) < 0) {
+                       res->success = FALSE;
+                       res->error = t_strdup_printf(
+                               "Malformed number '%s' in expires_in",
+                               field->value);
+                       return FALSE;
+               } else {
+                       res->expires_at = ioloop_time + expires_in;
+               }
+       } else if (strcasecmp(field->name, "token_type") == 0) {
+               if (strcasecmp(field->value,"bearer") != 0) {
+                       res->success = FALSE;
+                       res->error = t_strdup_printf(
+                               "Expected Bearer token, got '%s'",
+                               field->value);
+                       return FALSE;
+               }
+       } else if (strcasecmp(field->name, "access_token") == 0) {
+                       /* pooled memory */
+                       res->bearer_token = field->value;
+       }
+       return TRUE;
+}
+
+static void
+oauth2_refresh_continue(struct oauth2_request *req, bool success,
+                       const char *error)
+{
+       struct oauth2_refresh_result res;
+       i_zero(&res);
+
+       res.success = success;
+       res.error = error;
+
+       if (res.success) {
+               const struct oauth2_field *field;
+               /* see if we can figure out when it expires */
+               array_foreach(&req->fields, field) {
+                       if (!oauth2_refresh_field_parse(field, &res))
+                               break;
+               }
+       }
+
+       res.fields = &req->fields;
+
+       oauth2_refresh_callback(req, &res);
+}
+
+static void
+oauth2_refresh_response(const struct http_response *response,
+                       struct oauth2_request *req)
+{
+       if (response->status / 100 != 2) {
+               oauth2_refresh_continue(req, FALSE, response->reason);
+       } else {
+               if (response->payload == NULL) {
+                       oauth2_refresh_continue(req, FALSE, "Missing response body");
+                       return;
+               }
+               req->is = response->payload;
+               i_stream_ref(req->is);
+               req->parser = json_parser_init(req->is);
+               req->json_parsed_cb = oauth2_refresh_continue;
+               req->io = io_add_istream(req->is, oauth2_parse_json, req);
+               oauth2_parse_json(req);
+       }
+}
+
+#undef oauth2_refresh_start
+struct oauth2_request*
+oauth2_refresh_start(const struct oauth2_settings *set,
+                    const struct oauth2_request_input *input,
+                    oauth2_refresh_callback_t *callback,
+                    void *context)
+{
+       i_assert(oauth2_valid_token(input->token));
+
+       pool_t pool = pool_alloconly_create_clean("oauth2 refresh", 1024);
+       struct oauth2_request *req =
+               p_new(pool, struct oauth2_request, 1);
+       struct http_url *url;
+       const char *error;
+       struct oauth2_refresh_result fail = {
+               .success = FALSE
+       };
+
+       req->pool = pool;
+       req->set = set;
+       req->re_callback = callback;
+       req->re_context = context;
+
+       const char *_url = req->set->refresh_url;
+
+       if (http_url_parse(_url, NULL, 0, pool, &url, &error) < 0) {
+               fail.error = t_strdup_printf("http_url_parse(%s) failed: %s",
+                                            _url, error);
+               oauth2_refresh_callback(req, &fail);
+               return req;
+       }
+
+       req->req = http_client_request_url(req->set->client, "POST", url,
+                                          oauth2_refresh_response,
+                                              req);
+       string_t *payload = str_new(req->pool, 128);
+       str_append(payload, "client_secret=");
+       http_url_escape_param(payload, req->set->client_secret);
+       str_append(payload, "&grant_type=refresh_token&refresh_token=");
+       http_url_escape_param(payload, input->token);
+       str_append(payload, "&client_id=");
+       http_url_escape_param(payload, req->set->client_id);
+
+       struct istream *is = i_stream_create_from_string(payload);
+
+       http_client_request_add_header(req->req, "Content-Type",
+                                      "application/x-www-form-urlencoded");
+
+       oauth2_request_set_headers(req, input);
+
+       http_client_request_set_payload(req->req, is, FALSE);
+       i_stream_unref(&is);
+       http_client_request_set_timeout_msecs(req->req,
+                                             req->set->timeout_msecs);
+       http_client_request_submit(req->req);
+
+       return req;
+}
diff --git a/src/lib-oauth2/oauth2-token-validate.c b/src/lib-oauth2/oauth2-token-validate.c
new file mode 100644 (file)
index 0000000..7c68018
--- /dev/null
@@ -0,0 +1,138 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "strnum.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "json-parser.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+
+static void
+oauth2_token_validation_callback(struct oauth2_request *req,
+                                struct oauth2_token_validation_result *res)
+{
+       i_assert(res->success == (res->error == NULL));
+       i_assert(req->tv_callback != NULL);
+       oauth2_token_validation_callback_t *callback = req->tv_callback;
+       req->tv_callback = NULL;
+       callback(res, req->tv_context);
+       oauth2_request_free_internal(req);
+}
+
+static void
+oauth2_token_validate_continue(struct oauth2_request *req, bool success,
+                              const char *error)
+{
+       struct oauth2_token_validation_result res;
+       i_zero(&res);
+
+       res.success = success;
+       res.error = error;
+       res.valid = req->valid;
+
+       if (res.success) {
+               const struct oauth2_field *field;
+               /* see if we can figure out when it expires */
+               array_foreach(&req->fields, field) {
+                       if (strcasecmp(field->name, "expires_in") == 0) {
+                               uint32_t expires_in = 0;
+                               if (str_to_uint32(field->value, &expires_in) < 0) {
+                                       res.success = FALSE;
+                                       res.error = "Malformed number in expires_in";
+                               } else {
+                                       res.expires_at = ioloop_time + expires_in;
+                               }
+                               break;
+                       }
+               }
+       }
+
+       res.fields = &req->fields;
+
+       oauth2_token_validation_callback(req, &res);
+}
+
+static void
+oauth2_token_validate_response(const struct http_response *response,
+                              struct oauth2_request *req)
+{
+       unsigned int status_1 = response->status / 100;
+
+       if (status_1 != 2 && status_1 != 4) {
+               oauth2_token_validate_continue(req, FALSE, response->reason);
+       } else {
+               if (status_1 == 2)
+                       req->valid = TRUE;
+               else
+                       req->valid = FALSE;
+               /* 2xx is sufficient for token validation */
+               if (response->payload == NULL) {
+                       p_array_init(&req->fields, req->pool, 1);
+                       oauth2_token_validate_continue(req, TRUE, NULL);
+                       return;
+               }
+               req->is = response->payload;
+               i_stream_ref(req->is);
+               req->parser = json_parser_init(req->is);
+               req->json_parsed_cb = oauth2_token_validate_continue;
+               req->io = io_add_istream(req->is, oauth2_parse_json, req);
+               oauth2_parse_json(req);
+       }
+}
+
+#undef oauth2_token_validation_start
+struct oauth2_request*
+oauth2_token_validation_start(const struct oauth2_settings *set,
+                             const struct oauth2_request_input *input,
+                             oauth2_token_validation_callback_t *callback,
+                             void *context)
+{
+       i_assert(oauth2_valid_token(input->token));
+
+       struct http_url *url;
+       const char *error;
+       struct oauth2_token_validation_result fail = {
+               .success = FALSE
+       };
+
+       pool_t pool = pool_alloconly_create_clean("oauth2 token_validation", 1024);
+       struct oauth2_request *req =
+               p_new(pool, struct oauth2_request, 1);
+
+       req->pool = pool;
+       req->set = set;
+       req->tv_callback = callback;
+       req->tv_context = context;
+
+       string_t *enc = t_str_new(64);
+       str_append(enc, req->set->tokeninfo_url);
+       http_url_escape_param(enc, input->token);
+
+       if (http_url_parse(str_c(enc), NULL, 0, pool, &url, &error) < 0) {
+               fail.error = t_strdup_printf("http_url_parse(%s) failed: %s",
+                                            str_c(enc), error);
+               oauth2_token_validation_callback(req, &fail);
+               return req;
+       }
+
+       req->req = http_client_request_url(req->set->client, "GET", url,
+                                          oauth2_token_validate_response,
+                                          req);
+       http_client_request_add_header(req->req,
+                                      "Authorization",
+                                      t_strdup_printf("Bearer %s",
+                                                      input->token));
+
+       oauth2_request_set_headers(req, input);
+
+       http_client_request_set_timeout_msecs(req->req,
+                                             req->set->timeout_msecs);
+       http_client_request_submit(req->req);
+
+       return req;
+}
+
diff --git a/src/lib-oauth2/oauth2.c b/src/lib-oauth2/oauth2.c
new file mode 100644 (file)
index 0000000..9cd54ca
--- /dev/null
@@ -0,0 +1,105 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "http-client.h"
+#include "json-parser.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+#include "safe-memset.h"
+
+void
+oauth2_parse_json(struct oauth2_request *req)
+{
+       bool success;
+       enum json_type type;
+       const char *token, *error;
+       int ret;
+
+       req->field_name = NULL;
+
+       while((ret = json_parse_next(req->parser, &type, &token)) > 0) {
+               if (req->field_name == NULL) {
+                       if (type != JSON_TYPE_OBJECT_KEY) break;
+                       /* cannot use t_strdup because we might
+                          have to read more */
+                       req->field_name = p_strdup(req->pool, token);
+               } else if (type < JSON_TYPE_STRING) {
+                       /* this should be last allocation */
+                       p_free(req->pool, req->field_name);
+                       json_parse_skip_next(req->parser);
+               } else {
+                       if (!array_is_created(&req->fields))
+                               p_array_init(&req->fields, req->pool, 4);
+                       struct oauth2_field *field =
+                               array_append_space(&req->fields);
+                       field->name = req->field_name;
+                       req->field_name = NULL;
+                       field->value = p_strdup(req->pool, token);
+               }
+       }
+
+       /* read more */
+       if (ret == 0) return;
+
+       io_remove(&req->io);
+
+       if (ret > 0) {
+               (void)json_parser_deinit(&req->parser, &error);
+               error = "Invalid response data";
+               success = FALSE;
+       } else {
+               ret = json_parser_deinit(&req->parser, &error);
+               success = (ret == 0);
+       }
+
+       i_stream_unref(&req->is);
+
+       req->json_parsed_cb(req, success, error);
+}
+
+void
+oauth2_request_abort(struct oauth2_request **_req)
+{
+       struct oauth2_request *req = *_req;
+       *_req = NULL;
+
+       if (req->req != NULL)
+               http_client_request_abort(&req->req);
+       oauth2_request_free_internal(req);
+}
+
+void
+oauth2_request_free_internal(struct oauth2_request *req)
+{
+       pool_unref(&req->pool);
+}
+
+bool oauth2_valid_token(const char *token)
+{
+       if (token == NULL || *token == '\0' || strpbrk(token, "\r\n") != NULL)
+               return FALSE;
+       return TRUE;
+}
+
+void oauth2_request_set_headers(struct oauth2_request *req,
+                               const struct oauth2_request_input *input)
+{
+       if (!req->set->send_auth_headers)
+               return;
+       if (input->service != NULL) {
+               http_client_request_add_header(req->req, "X-Dovecot-Auth-Service",
+                                              input->service);
+       }
+       if (input->local_ip.family != 0) {
+               const char *addr;
+               if (net_ipport2str(&input->local_ip, input->local_port, &addr) == 0)    
+                       http_client_request_add_header(req->req, "X-Dovecot-Auth-Local", addr);
+       }
+       if (input->remote_ip.family != 0) {
+               const char *addr;
+               if (net_ipport2str(&input->remote_ip, input->remote_port, &addr) == 0)    
+                       http_client_request_add_header(req->req, "X-Dovecot-Auth-Remote", addr);
+       }
+}
diff --git a/src/lib-oauth2/oauth2.h b/src/lib-oauth2/oauth2.h
new file mode 100644 (file)
index 0000000..7be0b5f
--- /dev/null
@@ -0,0 +1,110 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+#ifndef OAUTH2_H
+#define OAUTH2_H
+
+#include "net.h"
+
+struct oauth2_request;
+
+struct oauth2_field {
+       const char *name;
+       const char *value;
+};
+
+ARRAY_DEFINE_TYPE(oauth2_field, struct oauth2_field);
+
+struct oauth2_settings {
+       struct http_client *client;
+       /* GET tokeninfo from this URL, token is appended to URL
+          http://some.host/path?access_token= */
+       const char *tokeninfo_url;
+       /* GET more information from this URL, uses Bearer authentication */
+       const char *introspection_url;
+       /* POST refresh here, needs refresh token and client_* settings */
+       const char *refresh_url;
+       const char *client_id;
+       const char *client_secret;
+       enum {
+               INTROSPECTION_MODE_GET_AUTH,
+               INTROSPECTION_MODE_GET,
+               INTROSPECTION_MODE_POST
+       } introspection_mode;
+       unsigned int timeout_msecs;
+       /* Should X-Dovecot-Auth-* headers be sent */
+       bool send_auth_headers;
+};
+
+struct oauth2_token_validation_result {
+       ARRAY_TYPE(oauth2_field) *fields;
+       const char *error;
+       time_t expires_at;
+       bool success:1;
+       bool valid:1;
+};
+
+struct oauth2_introspection_result {
+       ARRAY_TYPE(oauth2_field) *fields;
+       const char *error;
+       bool success:1;
+};
+
+struct oauth2_refresh_result {
+       ARRAY_TYPE(oauth2_field) *fields;
+       const char *bearer_token;
+       const char *error;
+       time_t expires_at;
+       bool success:1;
+};
+
+struct oauth2_request_input {
+       const char *token;
+       const char *service;
+       struct ip_addr local_ip, real_local_ip, remote_ip, real_remote_ip;
+       in_port_t local_port, real_local_port, remote_port, real_remote_port;
+};
+
+typedef void
+oauth2_token_validation_callback_t(struct oauth2_token_validation_result*, void*);
+
+typedef void
+oauth2_introspection_callback_t(struct oauth2_introspection_result*, void*);
+
+typedef void
+oauth2_refresh_callback_t(struct oauth2_refresh_result*, void*);
+
+bool oauth2_valid_token(const char *token);
+
+struct oauth2_request*
+oauth2_token_validation_start(const struct oauth2_settings *set,
+                             const struct oauth2_request_input *input,
+                             oauth2_token_validation_callback_t *callback,
+                             void *context);
+#define oauth2_token_validation_start(set, input, callback, context) \
+       oauth2_token_validation_start(set, input + \
+               CALLBACK_TYPECHECK(callback, void(*)(struct oauth2_token_validation_result*, typeof(context))), \
+               (oauth2_token_validation_callback_t*)callback, (void*)context);
+
+struct oauth2_request*
+oauth2_introspection_start(const struct oauth2_settings *set,
+                          const struct oauth2_request_input *input,
+                          oauth2_introspection_callback_t *callback,
+                          void *context);
+#define oauth2_introspection_start(set, input, callback, context) \
+       oauth2_introspection_start(set, input + \
+               CALLBACK_TYPECHECK(callback, void(*)(struct oauth2_introspection_result*, typeof(context))), \
+               (oauth2_introspection_callback_t*)callback, (void*)context);
+
+struct oauth2_request*
+oauth2_refresh_start(const struct oauth2_settings *set,
+                    const struct oauth2_request_input *input,
+                    oauth2_refresh_callback_t *callback,
+                    void *context);
+#define oauth2_refresh_start(set, input, callback, context) \
+       oauth2_refresh_start(set, input + \
+               CALLBACK_TYPECHECK(callback, void(*)(struct oauth2_refresh_result*, typeof(context))), \
+               (oauth2_refresh_callback_t*)callback, (void*)context);
+
+/* abort without calling callback, use this to cancel the request */
+void oauth2_request_abort(struct oauth2_request **);
+
+#endif