From: Florian Forster Date: Sat, 20 Jun 2015 06:53:24 +0000 (+0200) Subject: src/utils_oauth.[ch]: Add utility for OAuth authentication. X-Git-Url: https://git.octo.it/?a=commitdiff_plain;h=aa61b9056d7d14d414e1f5b8f7636e5c5bb9301e;p=collectd.git src/utils_oauth.[ch]: Add utility for OAuth authentication. --- diff --git a/Makefile.am b/Makefile.am index 48a7cb3d..ce8c92be 100644 --- a/Makefile.am +++ b/Makefile.am @@ -574,6 +574,31 @@ liboconfig_la_SOURCES = \ liboconfig_la_CPPFLAGS = -I$(srcdir)/src/liboconfig $(AM_CPPFLAGS) liboconfig_la_LDFLAGS = -avoid-version $(LEXLIB) +if BUILD_WITH_LIBCURL +if BUILD_WITH_LIBSSL +if BUILD_WITH_LIBYAJL +noinst_LTLIBRARIES += liboauth.la +liboauth_la_SOURCES = \ + src/utils_oauth.c \ + src/utils_oauth.h +liboauth_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + $(BUILD_WITH_LIBCURL_CFLAGS) \ + $(BUILD_WITH_LIBSSL_CFLAGS) \ + $(BUILD_WITH_LIBYAJL_CPPFLAGS) +liboauth_la_LIBADD = \ + $(BUILD_WITH_LIBCURL_LIBS) \ + $(BUILD_WITH_LIBSSL_LIBS) \ + $(BUILD_WITH_LIBYAJL_LIBS) + +check_PROGRAMS += test_utils_oauth +TESTS += test_utils_oauth +test_utils_oauth_SOURCES = utils_oauth_test.c +test_utils_oauth_LDADD = liboauth.la daemon/libcommon.la daemon/libplugin_mock.la +endif +endif +endif + if BUILD_PLUGIN_AGGREGATION pkglib_LTLIBRARIES += aggregation.la diff --git a/configure.ac b/configure.ac index 95caefff..cae848f9 100644 --- a/configure.ac +++ b/configure.ac @@ -2304,6 +2304,8 @@ fi AC_SUBST(BUILD_WITH_LIBCURL_CFLAGS) AC_SUBST(BUILD_WITH_LIBCURL_LIBS) + +AM_CONDITIONAL([BUILD_WITH_LIBCURL], [test "x$with_libcurl" = "xyes"]) # }}} # --with-libdbi {{{ @@ -5234,6 +5236,59 @@ PKG_CHECK_MODULES([LIBSIGROK], [libsigrok < 0.4], ) # }}} +# --with-libssl {{{ +with_libssl_cflags="" +with_libssl_ldflags="" +AC_ARG_WITH(libssl, [AS_HELP_STRING([--with-libssl@<:@=PREFIX@:>@], [Path to libssl.])], +[ + if test "x$withval" != "xno" && test "x$withval" != "xyes" + then + with_libssl_cppflags="-I$withval/include" + with_libssl_ldflags="-L$withval/lib" + with_libssl="yes" + else + with_libssl="$withval" + fi +], +[ + with_libssl="yes" +]) +if test "x$with_libssl" = "xyes" +then + SAVE_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $with_libssl_cppflags" + + AC_CHECK_HEADERS(openssl/sha.h openssl/blowfish.h openssl/rand.h, + [with_libssl="yes"], + [with_libssl="no (ssl header not found)"]) + + CPPFLAGS="$SAVE_CPPFLAGS" +fi +if test "x$with_libssl" = "xyes" +then + SAVE_CPPFLAGS="$CPPFLAGS" + SAVE_LDFLAGS="$LDFLAGS" + CPPFLAGS="$CPPFLAGS $with_libssl_cppflags" + LDFLAGS="$LDFLAGS $with_libssl_ldflags" + + AC_CHECK_LIB(ssl, OPENSSL_init_ssl, [with_libssl="yes"], [with_libssl="no (Symbol 'SSL_library_init' not found)"]) + + CPPFLAGS="$SAVE_CPPFLAGS" + LDFLAGS="$SAVE_LDFLAGS" +fi +if test "x$with_libssl" = "xyes" +then + BUILD_WITH_LIBSSL_CFLAGS="$with_libssl_cflags" + BUILD_WITH_LIBSSL_LDFLAGS="$with_libssl_ldflags" + BUILD_WITH_LIBSSL_LIBS="-lssl -lcrypto" + AC_SUBST(BUILD_WITH_LIBSSL_CFLAGS) + AC_SUBST(BUILD_WITH_LIBSSL_LDFLAGS) + AC_SUBST(BUILD_WITH_LIBSSL_LIBS) + AC_DEFINE(HAVE_LIBSSL, 1, [Define if libssl is present and usable.]) +fi +AM_CONDITIONAL(BUILD_WITH_LIBSSL, test "x$with_libssl" = "xyes") +# }}} + # --with-libstatgrab {{{ AC_ARG_WITH([libstatgrab], [AS_HELP_STRING([--with-libstatgrab@<:@=PREFIX@:>@], [Path to libstatgrab.])], @@ -7096,6 +7151,7 @@ AC_MSG_RESULT([ librouteros . . . . . $with_librouteros]) AC_MSG_RESULT([ librrd . . . . . . . $with_librrd]) AC_MSG_RESULT([ libsensors . . . . . $with_libsensors]) AC_MSG_RESULT([ libsigrok . . . . . $with_libsigrok]) +AC_MSG_RESULT([ libssl . . . . . . . $with_libssl]) AC_MSG_RESULT([ libstatgrab . . . . . $with_libstatgrab]) AC_MSG_RESULT([ libtokyotyrant . . . $with_libtokyotyrant]) AC_MSG_RESULT([ libudev . . . . . . . $with_libudev]) diff --git a/src/utils_oauth.c b/src/utils_oauth.c new file mode 100644 index 00000000..c5eae067 --- /dev/null +++ b/src/utils_oauth.c @@ -0,0 +1,693 @@ +/** + * collectd - src/utils_oauth.c + * ISC license + * + * Copyright (C) 2017 Florian Forster + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Authors: + * Florian Forster + **/ + +#include "collectd.h" + +#include "common.h" +#include "plugin.h" +#include "utils_oauth.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +/* + * Private variables + */ +#define GOOGLE_TOKEN_URL "https://accounts.google.com/o/oauth2/token" + +/* Max send buffer size, since there will be only one writer thread and + * monitoring api supports up to 100K bytes in one request, 64K is reasonable + */ +#define MAX_BUFFER_SIZE 65536 +#define MAX_ENCODE_SIZE 2048 + +struct oauth_s { + char *url; + char *iss; + char *aud; + char *scope; + + EVP_PKEY *key; + + char *token; + cdtime_t valid_until; +}; + +struct memory_s { + char *memory; + size_t size; +}; +typedef struct memory_s memory_t; + +#define OAUTH_GRANT_TYPE "urn:ietf:params:oauth:grant-type:jwt-bearer" +#define OAUTH_EXPIRATION_TIME TIME_T_TO_CDTIME_T(3600) +#define OAUTH_HEADER "{\"alg\":\"RS256\",\"typ\":\"JWT\"}" + +static const char OAUTH_CLAIM_FORMAT[] = "{" + "\"iss\":\"%s\"," + "\"scope\":\"%s\"," + "\"aud\":\"%s\"," + "\"exp\":%lu," + "\"iat\":%lu" + "}"; + +static size_t write_memory(void *contents, size_t size, size_t nmemb, /* {{{ */ + void *userp) { + size_t realsize = size * nmemb; + memory_t *mem = (memory_t *)userp; + char *tmp; + + if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) { + ERROR("integer overflow"); + return 0; + } + + tmp = (char *)realloc((void *)mem->memory, mem->size + realsize + 1); + if (tmp == NULL) { + /* out of memory! */ + ERROR("write_memory: not enough memory (realloc returned NULL)"); + return 0; + } + mem->memory = tmp; + + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + + return realsize; +} /* }}} size_t write_memory */ + +static EVP_PKEY *load_p12(/* {{{ */ + char const *p12_filename, + char const *p12_passphrase) { + FILE *fp; + PKCS12 *p12; + X509 *cert; + STACK_OF(X509) *ca = NULL; + EVP_PKEY *pkey = NULL; + + OpenSSL_add_all_algorithms(); + + fp = fopen(p12_filename, "rb"); + if (fp == NULL) { + char errbuf[1024]; + ERROR("utils_oauth: Opening private key %s failed: %s", p12_filename, + sstrerror(errno, errbuf, sizeof(errbuf))); + return NULL; + } + + p12 = d2i_PKCS12_fp(fp, NULL); + fclose(fp); + if (p12 == NULL) { + char errbuf[1024]; + ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf)); + ERROR("utils_oauth: Reading private key %s failed: %s", p12_filename, + errbuf); + return NULL; + } + + if (PKCS12_parse(p12, p12_passphrase, &pkey, &cert, &ca) == 0) { + char errbuf[1024]; + ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf)); + ERROR("utils_oauth: Parsing private key %s failed: %s", p12_filename, + errbuf); + + if (cert) + X509_free(cert); + if (ca) + sk_X509_pop_free(ca, X509_free); + PKCS12_free(p12); + return NULL; + } + + return pkey; +} /* }}} EVP_PKEY *load_p12 */ + +/* Base64-encodes "s" and stores the result in buffer. + * Returns zero on success, non-zero otherwise. */ +static int base64_encode_n(char const *s, size_t s_size, /* {{{ */ + char *buffer, size_t buffer_size) { + BIO *b64; + BUF_MEM *bptr; + int status; + size_t i; + + /* Set up the memory-base64 chain */ + b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + b64 = BIO_push(b64, BIO_new(BIO_s_mem())); + + /* Write data to the chain */ + BIO_write(b64, (void const *)s, s_size); + status = BIO_flush(b64); + if (status != 1) { + ERROR("utils_oauth: base64_encode: BIO_flush() failed."); + BIO_free_all(b64); + return -1; + } + + /* Never fails */ + BIO_get_mem_ptr(b64, &bptr); + + if (buffer_size <= bptr->length) { + ERROR("utils_oauth: base64_encode: Buffer too small."); + BIO_free_all(b64); + return -1; + } + + /* Copy data to buffer. */ + memcpy(buffer, bptr->data, bptr->length); + buffer[bptr->length] = 0; + + /* replace + with -, / with _ and remove padding = at the end */ + for (i = 0; i < bptr->length; i++) { + if (buffer[i] == '+') { + buffer[i] = '-'; + } else if (buffer[i] == '/') { + buffer[i] = '_'; + } else if (buffer[i] == '=') { + buffer[i] = 0; + } + } + + BIO_free_all(b64); + return 0; +} /* }}} int base64_encode_n */ + +/* Base64-encodes "s" and stores the result in buffer. + * Returns zero on success, non-zero otherwise. */ +static int base64_encode(char const *s, /* {{{ */ + char *buffer, size_t buffer_size) { + return base64_encode_n(s, strlen(s), buffer, buffer_size); +} /* }}} int base64_encode */ + +/* get_header returns the base64 encoded OAuth header. */ +static int get_header(char *buffer, size_t buffer_size) /* {{{ */ +{ + char header[] = OAUTH_HEADER; + + return base64_encode(header, buffer, buffer_size); +} /* }}} int get_header */ + +/* get_claim constructs an OAuth claim and returns it as base64 encoded string. + */ +static int get_claim(oauth_t *auth, char *buffer, size_t buffer_size) /* {{{ */ +{ + char claim[buffer_size]; + cdtime_t exp; + cdtime_t iat; + int status; + + iat = cdtime(); + exp = iat + OAUTH_EXPIRATION_TIME; + + /* create the claim set */ + status = + snprintf(claim, sizeof(claim), OAUTH_CLAIM_FORMAT, auth->iss, auth->scope, + auth->aud, (unsigned long)CDTIME_T_TO_TIME_T(exp), + (unsigned long)CDTIME_T_TO_TIME_T(iat)); + if (status < 1) + return -1; + else if ((size_t)status >= sizeof(claim)) + return ENOMEM; + + DEBUG("utils_oauth: get_claim() = %s", claim); + + return base64_encode(claim, buffer, buffer_size); +} /* }}} int get_claim */ + +/* get_signature signs header and claim with pkey and returns the signature in + * buffer. */ +static int get_signature(char *buffer, size_t buffer_size, /* {{{ */ + char const *header, char const *claim, + EVP_PKEY *pkey) { + char payload[buffer_size]; + size_t payload_len; + char signature[buffer_size]; + unsigned int signature_size; + int status; + + /* Make the string to sign */ + payload_len = snprintf(payload, sizeof(payload), "%s.%s", header, claim); + if (payload_len < 1) { + return -1; + } else if (payload_len >= sizeof(payload)) { + return ENOMEM; + } + + /* Create the signature */ + signature_size = EVP_PKEY_size(pkey); + if (signature_size > sizeof(signature)) { + ERROR("utils_oauth: Signature is too large (%u bytes).", signature_size); + return -1; + } + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + /* EVP_SignInit(3SSL) claims this is a void function, but in fact it returns + * an int. We're not going to rely on this, though. */ + EVP_SignInit(ctx, EVP_sha256()); + + status = EVP_SignUpdate(ctx, payload, payload_len); + if (status != 1) { + char errbuf[1024]; + ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf)); + ERROR("utils_oauth: EVP_SignUpdate failed: %s", errbuf); + + EVP_MD_CTX_free(ctx); + return -1; + } + + status = + EVP_SignFinal(ctx, (unsigned char *)signature, &signature_size, pkey); + if (status != 1) { + char errbuf[1024]; + ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf)); + ERROR("utils_oauth: EVP_SignFinal failed: %s", errbuf); + + EVP_MD_CTX_free(ctx); + return -1; + } + + EVP_MD_CTX_free(ctx); + + return base64_encode_n(signature, (size_t)signature_size, buffer, + buffer_size); +} /* }}} int get_signature */ + +static int get_assertion(oauth_t *auth, char *buffer, + size_t buffer_size) /* {{{ */ +{ + char header[buffer_size]; + char claim[buffer_size]; + char signature[buffer_size]; + int status; + + status = get_header(header, sizeof(header)); + if (status != 0) + return -1; + + status = get_claim(auth, claim, sizeof(claim)); + if (status != 0) + return -1; + + status = + get_signature(signature, sizeof(signature), header, claim, auth->key); + if (status != 0) + return -1; + + status = snprintf(buffer, buffer_size, "%s.%s.%s", header, claim, signature); + if (status < 1) + return -1; + else if (status >= buffer_size) + return ENOMEM; + + return 0; +} /* }}} int get_assertion */ + +int oauth_parse_json_token(char const *json, /* {{{ */ + char *out_access_token, size_t access_token_size, + cdtime_t *expires_in) { + time_t expire_in_seconds = 0; + yajl_val root; + yajl_val token_val; + yajl_val expire_val; + char errbuf[1024]; + const char *token_path[] = {"access_token", NULL}; + const char *expire_path[] = {"expires_in", NULL}; + + root = yajl_tree_parse(json, errbuf, sizeof(errbuf)); + if (root == NULL) { + ERROR("utils_oauth: oauth_parse_json_token: parse error %s", errbuf); + return -1; + } + + token_val = yajl_tree_get(root, token_path, yajl_t_string); + if (token_val == NULL) { + ERROR("utils_oauth: oauth_parse_json_token: access token field not found"); + yajl_tree_free(root); + return -1; + } + sstrncpy(out_access_token, YAJL_GET_STRING(token_val), access_token_size); + + expire_val = yajl_tree_get(root, expire_path, yajl_t_number); + if (expire_val == NULL) { + ERROR("utils_oauth: oauth_parse_json_token: expire field found"); + yajl_tree_free(root); + return -1; + } + expire_in_seconds = (time_t)YAJL_GET_INTEGER(expire_val); + DEBUG("oauth_parse_json_token: expires_in %lu", + (unsigned long)expire_in_seconds); + + *expires_in = TIME_T_TO_CDTIME_T(expire_in_seconds); + yajl_tree_free(root); + return 0; +} /* }}} int oauth_parse_json_token */ + +static int new_token(oauth_t *auth) /* {{{ */ +{ + CURL *curl; + char assertion[1024]; + char post_data[1024]; + memory_t data; + char access_token[256]; + cdtime_t expires_in; + cdtime_t now; + char curl_errbuf[CURL_ERROR_SIZE]; + int status = 0; + + data.size = 0; + data.memory = NULL; + + now = cdtime(); + + status = get_assertion(auth, assertion, sizeof(assertion)); + if (status != 0) { + ERROR("utils_oauth: Failed to get token using service account %s.", + auth->iss); + return -1; + } + + snprintf(post_data, sizeof(post_data), "grant_type=%s&assertion=%s", + OAUTH_GRANT_TYPE, assertion); + + curl = curl_easy_init(); + if (curl == NULL) { + ERROR("utils_oauth: curl_easy_init failed."); + return -1; + } + + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_memory); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); + curl_easy_setopt(curl, CURLOPT_URL, auth->url); + + status = curl_easy_perform(curl); + if (status != CURLE_OK) { + ERROR("utils_oauth: curl_easy_perform failed with status %i: %s", status, + curl_errbuf); + + sfree(data.memory); + curl_easy_cleanup(curl); + + return -1; + } else { + long http_code = 0; + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if ((http_code < 200) || (http_code >= 300)) { + ERROR("utils_oauth: POST request to %s failed: HTTP error %ld", auth->url, + http_code); + if (data.memory != NULL) + INFO("utils_oauth: Server replied: %s", data.memory); + + sfree(data.memory); + curl_easy_cleanup(curl); + + return -1; + } + } + + status = oauth_parse_json_token(data.memory, access_token, + sizeof(access_token), &expires_in); + if (status != 0) { + sfree(data.memory); + curl_easy_cleanup(curl); + + return -1; + } + + sfree(auth->token); + auth->token = strdup(access_token); + if (auth->token == NULL) { + ERROR("utils_oauth: strdup failed"); + auth->valid_until = 0; + + sfree(data.memory); + curl_easy_cleanup(curl); + return -1; + } + + INFO("utils_oauth: OAuth2 access token is valid for %.3fs", + CDTIME_T_TO_DOUBLE(expires_in)); + auth->valid_until = now + expires_in; + + sfree(data.memory); + curl_easy_cleanup(curl); + + return 0; +} /* }}} int new_token */ + +static int renew_token(oauth_t *auth) /* {{{ */ +{ + /* TODO(octo): Make sure that we get a new token 60 seconds or so before the + * old one expires. */ + if (auth->valid_until > cdtime()) + return 0; + + return new_token(auth); +} /* }}} int renew_token */ + +/* + * Public + */ +oauth_t *oauth_create(char const *url, char const *iss, char const *scope, + char const *aud, EVP_PKEY *key) /* {{{ */ +{ + oauth_t *auth; + + if ((url == NULL) || (iss == NULL) || (scope == NULL) || (aud == NULL) || + (key == NULL)) + return NULL; + + auth = malloc(sizeof(*auth)); + if (auth == NULL) + return NULL; + memset(auth, 0, sizeof(*auth)); + + auth->url = strdup(url); + auth->iss = strdup(iss); + auth->scope = strdup(scope); + auth->aud = strdup(aud); + + if ((auth->url == NULL) || (auth->iss == NULL) || (auth->scope == NULL) || + (auth->aud == NULL)) { + oauth_destroy(auth); + return NULL; + } + + auth->key = key; + + return auth; +} /* }}} oauth_t *oauth_create */ + +oauth_t *oauth_create_p12(char const *url, char const *iss, char const *scope, + char const *aud, /* {{{ */ + char const *file, char const *pass) { + EVP_PKEY *key = load_p12(file, pass); + if (key == NULL) { + ERROR("utils_oauth: Failed to load PKCS#12 key from %s", file); + return NULL; + } + + return oauth_create(url, iss, scope, aud, key); +} /* }}} oauth_t *oauth_create_p12 */ + +oauth_google_t oauth_create_google_json(char const *buffer, char const *scope) { + char errbuf[1024]; + yajl_val root = yajl_tree_parse(buffer, errbuf, sizeof(errbuf)); + if (root == NULL) { + ERROR("utils_oauth: oauth_create_google_json: parse error %s", errbuf); + return (oauth_google_t){NULL}; + } + + yajl_val field_project = + yajl_tree_get(root, (char const *[]){"project_id", NULL}, yajl_t_string); + if (field_project == NULL) { + ERROR("utils_oauth: oauth_create_google_json: project_id field not found"); + yajl_tree_free(root); + return (oauth_google_t){NULL}; + } + char const *project_id = YAJL_GET_STRING(field_project); + + yajl_val field_iss = yajl_tree_get( + root, (char const *[]){"client_email", NULL}, yajl_t_string); + if (field_iss == NULL) { + ERROR( + "utils_oauth: oauth_create_google_json: client_email field not found"); + yajl_tree_free(root); + return (oauth_google_t){NULL}; + } + + yajl_val field_token_uri = + yajl_tree_get(root, (char const *[]){"token_uri", NULL}, yajl_t_string); + char const *token_uri = (field_token_uri != NULL) + ? YAJL_GET_STRING(field_token_uri) + : GOOGLE_TOKEN_URL; + + yajl_val field_priv_key = + yajl_tree_get(root, (char const *[]){"private_key", NULL}, yajl_t_string); + if (field_priv_key == NULL) { + ERROR("utils_oauth: oauth_create_google_json: private_key field not found"); + yajl_tree_free(root); + return (oauth_google_t){NULL}; + } + + BIO *bp = BIO_new_mem_buf(YAJL_GET_STRING(field_priv_key), -1); + EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL); + if (pkey == NULL) { + char errbuf[1024]; + ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf)); + ERROR( + "utils_oauth: oauth_create_google_json: parsing private key failed: %s", + errbuf); + BIO_free(bp); + yajl_tree_free(root); + return (oauth_google_t){NULL}; + } + + BIO_free(bp); + + oauth_t *oauth = oauth_create(token_uri, YAJL_GET_STRING(field_iss), scope, + token_uri, pkey); + if (oauth == NULL) { + yajl_tree_free(root); + return (oauth_google_t){NULL}; + } + + oauth_google_t ret = { + .project_id = strdup(project_id), .oauth = oauth, + }; + + yajl_tree_free(root); + return ret; +} /* oauth_google_t oauth_create_google_json */ + +oauth_google_t oauth_create_google_file(char const *path, + char const *scope) { /* {{{ */ + int fd = open(path, O_RDONLY); + if (fd == -1) + return (oauth_google_t){NULL}; + + struct stat st = {0}; + if (fstat(fd, &st) != 0) { + close(fd); + return (oauth_google_t){NULL}; + } + + size_t buf_size = (size_t)st.st_size; + char *buf = calloc(1, buf_size + 1); + if (buf == NULL) { + close(fd); + return (oauth_google_t){NULL}; + } + + if (sread(fd, buf, buf_size) != 0) { + free(buf); + close(fd); + return (oauth_google_t){NULL}; + } + close(fd); + buf[buf_size] = 0; + + oauth_google_t ret = oauth_create_google_json(buf, scope); + + free(buf); + return ret; +} /* }}} oauth_google_t oauth_create_google_file */ + +/* oauth_create_google_default checks for JSON credentials in well-known + * positions, similar to gcloud and other tools. */ +oauth_google_t oauth_create_google_default(char const *scope) { + char const *app_creds; + if ((app_creds = getenv("GOOGLE_APPLICATION_CREDENTIALS")) != NULL) { + oauth_google_t ret = oauth_create_google_file(app_creds, scope); + if (ret.oauth == NULL) { + ERROR("The environment variable GOOGLE_APPLICATION_CREDENTIALS is set to " + "\"%s\" but that file could not be read.", + app_creds); + } else { + return ret; + } + } + + char const *home; + if ((home = getenv("HOME")) != NULL) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), + "%s/.config/gcloud/application_default_credentials.json", home); + + oauth_google_t ret = oauth_create_google_file(path, scope); + if (ret.oauth != NULL) { + return ret; + } + } + + return (oauth_google_t){NULL}; +} /* }}} oauth_google_t oauth_create_google_default */ + +void oauth_destroy(oauth_t *auth) /* {{{ */ +{ + if (auth == NULL) + return; + + sfree(auth->url); + sfree(auth->iss); + sfree(auth->scope); + sfree(auth->aud); + + if (auth->key != NULL) { + EVP_PKEY_free(auth->key); + auth->key = NULL; + } + + sfree(auth); +} /* }}} void oauth_destroy */ + +int oauth_access_token(oauth_t *auth, char *buffer, + size_t buffer_size) /* {{{ */ +{ + int status; + + if (auth == NULL) + return EINVAL; + + status = renew_token(auth); + if (status != 0) + return status; + assert(auth->token != NULL); + + sstrncpy(buffer, auth->token, buffer_size); + return 0; +} /* }}} int oauth_access_token */ diff --git a/src/utils_oauth.h b/src/utils_oauth.h new file mode 100644 index 00000000..ed61ee8e --- /dev/null +++ b/src/utils_oauth.h @@ -0,0 +1,73 @@ +/** + * collectd - src/utils_oauth.h + * ISC license + * + * Copyright (C) 2017 Florian Forster + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Authors: + * Florian Forster + **/ + +#ifndef UTILS_OAUTH_H +#define UTILS_OAUTH_H + +#include "collectd.h" +#include "utils_time.h" + +#ifndef GOOGLE_OAUTH_URL +#define GOOGLE_OAUTH_URL "https://www.googleapis.com/oauth2/v3/token" +#endif + +#include + +struct oauth_s; +typedef struct oauth_s oauth_t; + +int oauth_parse_json_token(char const *json, char *out_access_token, + size_t access_token_size, cdtime_t *expires_in); + +oauth_t *oauth_create(char const *url, char const *iss, char const *scope, + char const *aud, EVP_PKEY *key); +oauth_t *oauth_create_p12(char const *url, char const *iss, char const *scope, + char const *aud, char const *file, char const *pass); + +typedef struct { + char *project_id; + oauth_t *oauth; +} oauth_google_t; + +/* oauth_create_google_json creates an OAuth object from JSON encoded + * credentials. */ +oauth_google_t oauth_create_google_json(char const *json, char const *scope); + +/* oauth_create_google_file reads path, which contains JSON encoded service + * account credentials, and returns an OAuth object. */ +oauth_google_t oauth_create_google_file(char const *path, char const *scope); + +/* oauth_create_google_default looks for service account credentials in a couple + * of well-known places and returns an OAuth object if found. The well known + * locations are: + * + * - ${GOOGLE_APPLICATION_CREDENTIALS} + * - ${HOME}/.config/gcloud/application_default_credentials.json + */ +oauth_google_t oauth_create_google_default(char const *scope); + +/* oauth_destroy frees all resources associated with an OAuth object. */ +void oauth_destroy(oauth_t *auth); + +int oauth_access_token(oauth_t *auth, char *buffer, size_t buffer_size); + +#endif diff --git a/src/utils_oauth_test.c b/src/utils_oauth_test.c new file mode 100644 index 00000000..09c46e1d --- /dev/null +++ b/src/utils_oauth_test.c @@ -0,0 +1,146 @@ +/** + * collectd - src/tests/utils_oauth_test.c + * Copyright (C) 2015 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Authors: + * Florian Forster + **/ + +#include "testing.h" +#include "utils_oauth.h" + +struct { + char *json; + int status; + char *access_token; + cdtime_t expires_in; +} cases[] = { + { + "{\"access_token\":\"MaeC6kaePhie1ree\",\"expires_in\":3600}", + /* status = */ 0, "MaeC6kaePhie1ree", TIME_T_TO_CDTIME_T_STATIC(3600), + }, + { + "{\"token_type\":\"Bearer\",\"expires_in\":1800,\"access_token\":" + "\"aeThiebee2gushuY\"}", + /* status = */ 0, "aeThiebee2gushuY", TIME_T_TO_CDTIME_T_STATIC(1800), + }, + { + "{\"ignored_key\":\"uaph5aewaeghi1Ge\",\"expires_in\":3600}", + /* status = */ -1, NULL, 0, + }, + { + /* expires_in missing */ + "{\"access_token\":\"shaephohbie9Ahch\"}", + /* status = */ -1, NULL, 0, + }, +}; + +DEF_TEST(simple) /* {{{ */ +{ + size_t i; + _Bool success = 1; + + for (i = 0; i < (sizeof(cases) / sizeof(cases[0])); i++) { + char buffer[1024]; + cdtime_t expires_in; + + EXPECT_EQ_INT(cases[i].status, + oauth_parse_json_token(cases[i].json, buffer, sizeof(buffer), + &expires_in)); + if (cases[i].status != 0) + continue; + + EXPECT_EQ_STR(cases[i].access_token, buffer); + EXPECT_EQ_UINT64(cases[i].expires_in, expires_in); + } + + return success ? 0 : -1; +} /* }}} simple */ + +DEF_TEST(oauth_create_google_json) { + char const *in = + "{\"type\": \"service_account\"," + "\"project_id\":\"collectd.org:unit-test\"," + "\"private_key_id\": \"ed7b4eb6c1b61a7bedab5bcafff374f7fc820698\"," + "\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n" + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNvS71Lr2WIEqx\\n" + "U766iJGORVVib0FnHhOf/0FEI4Hw+tF11vP3LZj0AyQFIi/h2l2EDXOr43C6Gt+K\\n" + "0stsyaWvRNzeQa+dUFY5A/ZEtdvYVPq7KudML5Hs9DNmWFlM/iIfQyIUJ+vHv7fe\\n" + "pJGgu4ZgSkNWehmWj3qiRzIvYxKvDIQizqPZNlTh+33KQcT2x+ErkuB3snQu8hSK\\n" + "HAg2sCvORqKGOvN9F4bAqXt5T0NVjGy4YXeuif1p/Np/GH6Ys1p+etgGwvIimXIv\\n" + "jFL9K/ZtrTOcFdy4R5bwrj2piCZa2T5H6fupVp2tVgIuS53r2fEaBMLD97oAvwZ3\\n" + "9XPxG1NLAgMBAAECggEACgHroKcrN1FkdgyzSIKFG1evCBCOV17kqHyI5wYXzNTT\\n" + "zyNrZDjBFGQkt+U0/AucTznnnahSCZNuD+QiBgLRqYgJevwp99Z6YzVDS438Xsuq\\n" + "Ezmf3O+sGEu78Pys11cTP38LT3yuS4iSqo9Jus5JrTG05dDJoYO4J4rxW3xlDRj8\\n" + "lQUimXI+S9skaSusf0oErDrjuQG9dxmhnGcSEX+rIe9G0UygTNuI0KKGJ8jmnPz5\\n" + "OS+sM8qrKcnjrvENFWKLb11HlliHkh6dILoO5rvf5DR+XGKM7BFAsdWg6oI7SFGh\\n" + "S6zGZ0jUR7QAugrjbTlDOCnAuZ+Mbc/4yHZ3u5PlcQKBgQDuvH1ds1YmmbOllOK5\\n" + "JtkdjCUUyH1bgkMrmcg/KkRARPRHQvfAioZsC6d0fa6jq0kTW/3Zu14IsVXgM8xK\\n" + "fuNSp8LdY+NCtJnfvdLaChgAwZaQLX4qgV0qYw8iLv5ifa4ZY0qaZioJCzkv57y1\\n" + "KkavYvITboO7aUSa441Zko9c+wKBgQDcndg0QpWH6JMz/FkCf/KDyW/cUODfKXhP\\n" + "5p9eTcVlfDL2sAb2RzVhvKZcuWXVwnfaDP0oBj2/SBLGx0idUb+VHdM/IGiLroyK\\n" + "pAHpNM//dowiGL1qPPOLXrzF/vn+w4t2Dqggfcqu52SzRiyaxUtSMnNyyyU19cO+\\n" + "pb7wAS5x8QKBgCW7WL0UeQtEw6Xp8CN/RlVrLvkn7tglsGQVvBZvobXesBULOokN\\n" + "28z70o2Qx6dKjRQoN+jPuj75eC8lQKaNg3Qu25eOD/8c+CzqnYakjcKg1iEXb5dc\\n" + "NtNaMKwgbUg3wOp2TPY2K3KeeX1ezO59LgrOQqBbmSpnqtYoHNEJXus9AoGAWl/y\\n" + "9J2eIdm9i5tBX0vIrgHz5/3d0K1tUtX3zSrwxT0Wp4W+pF7RWGNuhyePtvx+Gn4d\\n" + "qqq72sMMpg93CLM3Vz+rjP2atjXf7t92xPDUkCMhDsqxtXaYkixSCo4EHUA/vjIM\\n" + "35qIUBQMZYBGv3Q5AcgXERx09uDhuhSt3iWtwBECgYAHFnCh8fKsJbQrVN10tU/h\\n" + "ofVx0KZkUpBz8eNQPuxt4aY+LyWsKVKtnduw2WdumuOY66cUN1lsi8Bz/cq1dhPt\\n" + "Oc2S7pqjbu2Q1Oqx+/yr6jqsvKaSxHmcpbWQBsGn6UaWZgYZcAtQBcqDAp7pylwj\\n" + "tejRh0NB8d81H5Dli1Qfzw==\\n" + "-----END PRIVATE KEY-----\\n\"," + "\"client_email\":\"example-sacct@unit-test.iam.gserviceaccount.com\", " + "\"client_id\": \"109958449193027604084\"," + "\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\"," + "\"token_uri\":\"https://accounts.google.com/o/oauth2/token\"," + "\"auth_provider_x509_cert_url\":" + "\"https://www.googleapis.com/oauth2/v1/certs\"," + "\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/" + "metadata/x509/example-sacct%40ssc-serv-dev.iam.gserviceaccount.com\"}"; + + oauth_t *ret = + oauth_create_google_json(in, "https://collectd.org/example.scope"); + CHECK_NOT_NULL(ret); + + struct { + char *url; + char *iss; + char *aud; + char *scope; + } *obj = (void *)ret; + + EXPECT_EQ_STR("https://accounts.google.com/o/oauth2/token", obj->url); + EXPECT_EQ_STR("example-sacct@unit-test.iam.gserviceaccount.com", obj->iss); + EXPECT_EQ_STR("https://collectd.org/example.scope", obj->scope); + + return 0; +} + +int main(int argc, char **argv) /* {{{ */ +{ + RUN_TEST(simple); + RUN_TEST(oauth_create_google_json); + + END_TEST; +} /* }}} int main */ + +/* vim: set sw=2 sts=2 et fdm=marker : */