2 * collectd - src/utils_oauth.c
5 * Copyright (C) 2017 Florian Forster
7 * Permission to use, copy, modify, and/or distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
11 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 * Florian Forster <octo at collectd.org>
27 #include "utils_oauth.h"
29 #include <curl/curl.h>
31 #include <yajl/yajl_tree.h>
32 #include <yajl/yajl_version.h>
34 #include <openssl/err.h>
35 #include <openssl/evp.h>
36 #include <openssl/pem.h>
37 #include <openssl/pkcs12.h>
38 #include <openssl/sha.h>
43 #define GOOGLE_TOKEN_URL "https://accounts.google.com/o/oauth2/token"
45 /* Max send buffer size, since there will be only one writer thread and
46 * monitoring api supports up to 100K bytes in one request, 64K is reasonable
48 #define MAX_BUFFER_SIZE 65536
49 #define MAX_ENCODE_SIZE 2048
67 typedef struct memory_s memory_t;
69 #define OAUTH_GRANT_TYPE "urn:ietf:params:oauth:grant-type:jwt-bearer"
70 #define OAUTH_EXPIRATION_TIME TIME_T_TO_CDTIME_T(3600)
71 #define OAUTH_HEADER "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"
73 static const char OAUTH_CLAIM_FORMAT[] = "{"
81 static size_t write_memory(void *contents, size_t size, size_t nmemb, /* {{{ */
83 size_t realsize = size * nmemb;
84 memory_t *mem = (memory_t *)userp;
87 if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) {
88 ERROR("integer overflow");
92 tmp = (char *)realloc((void *)mem->memory, mem->size + realsize + 1);
95 ERROR("write_memory: not enough memory (realloc returned NULL)");
100 memcpy(&(mem->memory[mem->size]), contents, realsize);
101 mem->size += realsize;
102 mem->memory[mem->size] = 0;
105 } /* }}} size_t write_memory */
107 static EVP_PKEY *load_p12(/* {{{ */
108 char const *p12_filename,
109 char const *p12_passphrase) {
113 STACK_OF(X509) *ca = NULL;
114 EVP_PKEY *pkey = NULL;
116 OpenSSL_add_all_algorithms();
118 fp = fopen(p12_filename, "rb");
121 ERROR("utils_oauth: Opening private key %s failed: %s", p12_filename,
122 sstrerror(errno, errbuf, sizeof(errbuf)));
126 p12 = d2i_PKCS12_fp(fp, NULL);
130 ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
131 ERROR("utils_oauth: Reading private key %s failed: %s", p12_filename,
136 if (PKCS12_parse(p12, p12_passphrase, &pkey, &cert, &ca) == 0) {
138 ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
139 ERROR("utils_oauth: Parsing private key %s failed: %s", p12_filename,
145 sk_X509_pop_free(ca, X509_free);
151 } /* }}} EVP_PKEY *load_p12 */
153 /* Base64-encodes "s" and stores the result in buffer.
154 * Returns zero on success, non-zero otherwise. */
155 static int base64_encode_n(char const *s, size_t s_size, /* {{{ */
156 char *buffer, size_t buffer_size) {
162 /* Set up the memory-base64 chain */
163 b64 = BIO_new(BIO_f_base64());
164 BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
165 b64 = BIO_push(b64, BIO_new(BIO_s_mem()));
167 /* Write data to the chain */
168 BIO_write(b64, (void const *)s, s_size);
169 status = BIO_flush(b64);
171 ERROR("utils_oauth: base64_encode: BIO_flush() failed.");
177 BIO_get_mem_ptr(b64, &bptr);
179 if (buffer_size <= bptr->length) {
180 ERROR("utils_oauth: base64_encode: Buffer too small.");
185 /* Copy data to buffer. */
186 memcpy(buffer, bptr->data, bptr->length);
187 buffer[bptr->length] = 0;
189 /* replace + with -, / with _ and remove padding = at the end */
190 for (i = 0; i < bptr->length; i++) {
191 if (buffer[i] == '+') {
193 } else if (buffer[i] == '/') {
195 } else if (buffer[i] == '=') {
202 } /* }}} int base64_encode_n */
204 /* Base64-encodes "s" and stores the result in buffer.
205 * Returns zero on success, non-zero otherwise. */
206 static int base64_encode(char const *s, /* {{{ */
207 char *buffer, size_t buffer_size) {
208 return base64_encode_n(s, strlen(s), buffer, buffer_size);
209 } /* }}} int base64_encode */
211 /* get_header returns the base64 encoded OAuth header. */
212 static int get_header(char *buffer, size_t buffer_size) /* {{{ */
214 char header[] = OAUTH_HEADER;
216 return base64_encode(header, buffer, buffer_size);
217 } /* }}} int get_header */
219 /* get_claim constructs an OAuth claim and returns it as base64 encoded string.
221 static int get_claim(oauth_t *auth, char *buffer, size_t buffer_size) /* {{{ */
223 char claim[buffer_size];
229 exp = iat + OAUTH_EXPIRATION_TIME;
231 /* create the claim set */
233 snprintf(claim, sizeof(claim), OAUTH_CLAIM_FORMAT, auth->iss, auth->scope,
234 auth->aud, (unsigned long)CDTIME_T_TO_TIME_T(exp),
235 (unsigned long)CDTIME_T_TO_TIME_T(iat));
238 else if ((size_t)status >= sizeof(claim))
241 DEBUG("utils_oauth: get_claim() = %s", claim);
243 return base64_encode(claim, buffer, buffer_size);
244 } /* }}} int get_claim */
246 /* get_signature signs header and claim with pkey and returns the signature in
248 static int get_signature(char *buffer, size_t buffer_size, /* {{{ */
249 char const *header, char const *claim,
251 char payload[buffer_size];
253 char signature[buffer_size];
254 unsigned int signature_size;
257 /* Make the string to sign */
258 payload_len = snprintf(payload, sizeof(payload), "%s.%s", header, claim);
259 if (payload_len < 1) {
261 } else if (payload_len >= sizeof(payload)) {
265 /* Create the signature */
266 signature_size = EVP_PKEY_size(pkey);
267 if (signature_size > sizeof(signature)) {
268 ERROR("utils_oauth: Signature is too large (%u bytes).", signature_size);
272 EVP_MD_CTX *ctx = EVP_MD_CTX_new();
274 /* EVP_SignInit(3SSL) claims this is a void function, but in fact it returns
275 * an int. We're not going to rely on this, though. */
276 EVP_SignInit(ctx, EVP_sha256());
278 status = EVP_SignUpdate(ctx, payload, payload_len);
281 ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
282 ERROR("utils_oauth: EVP_SignUpdate failed: %s", errbuf);
284 EVP_MD_CTX_free(ctx);
289 EVP_SignFinal(ctx, (unsigned char *)signature, &signature_size, pkey);
292 ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
293 ERROR("utils_oauth: EVP_SignFinal failed: %s", errbuf);
295 EVP_MD_CTX_free(ctx);
299 EVP_MD_CTX_free(ctx);
301 return base64_encode_n(signature, (size_t)signature_size, buffer,
303 } /* }}} int get_signature */
305 static int get_assertion(oauth_t *auth, char *buffer,
306 size_t buffer_size) /* {{{ */
308 char header[buffer_size];
309 char claim[buffer_size];
310 char signature[buffer_size];
313 status = get_header(header, sizeof(header));
317 status = get_claim(auth, claim, sizeof(claim));
322 get_signature(signature, sizeof(signature), header, claim, auth->key);
326 status = snprintf(buffer, buffer_size, "%s.%s.%s", header, claim, signature);
329 else if (status >= buffer_size)
333 } /* }}} int get_assertion */
335 int oauth_parse_json_token(char const *json, /* {{{ */
336 char *out_access_token, size_t access_token_size,
337 cdtime_t *expires_in) {
338 time_t expire_in_seconds = 0;
343 const char *token_path[] = {"access_token", NULL};
344 const char *expire_path[] = {"expires_in", NULL};
346 root = yajl_tree_parse(json, errbuf, sizeof(errbuf));
348 ERROR("utils_oauth: oauth_parse_json_token: parse error %s", errbuf);
352 token_val = yajl_tree_get(root, token_path, yajl_t_string);
353 if (token_val == NULL) {
354 ERROR("utils_oauth: oauth_parse_json_token: access token field not found");
355 yajl_tree_free(root);
358 sstrncpy(out_access_token, YAJL_GET_STRING(token_val), access_token_size);
360 expire_val = yajl_tree_get(root, expire_path, yajl_t_number);
361 if (expire_val == NULL) {
362 ERROR("utils_oauth: oauth_parse_json_token: expire field found");
363 yajl_tree_free(root);
366 expire_in_seconds = (time_t)YAJL_GET_INTEGER(expire_val);
367 DEBUG("oauth_parse_json_token: expires_in %lu",
368 (unsigned long)expire_in_seconds);
370 *expires_in = TIME_T_TO_CDTIME_T(expire_in_seconds);
371 yajl_tree_free(root);
373 } /* }}} int oauth_parse_json_token */
375 static int new_token(oauth_t *auth) /* {{{ */
378 char assertion[1024];
379 char post_data[1024];
381 char access_token[256];
384 char curl_errbuf[CURL_ERROR_SIZE];
392 status = get_assertion(auth, assertion, sizeof(assertion));
394 ERROR("utils_oauth: Failed to get token using service account %s.",
399 snprintf(post_data, sizeof(post_data), "grant_type=%s&assertion=%s",
400 OAUTH_GRANT_TYPE, assertion);
402 curl = curl_easy_init();
404 ERROR("utils_oauth: curl_easy_init failed.");
408 curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
409 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
410 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_memory);
411 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
412 curl_easy_setopt(curl, CURLOPT_POST, 1L);
413 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
414 curl_easy_setopt(curl, CURLOPT_URL, auth->url);
416 status = curl_easy_perform(curl);
417 if (status != CURLE_OK) {
418 ERROR("utils_oauth: curl_easy_perform failed with status %i: %s", status,
422 curl_easy_cleanup(curl);
428 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
429 if ((http_code < 200) || (http_code >= 300)) {
430 ERROR("utils_oauth: POST request to %s failed: HTTP error %ld", auth->url,
432 if (data.memory != NULL)
433 INFO("utils_oauth: Server replied: %s", data.memory);
436 curl_easy_cleanup(curl);
442 status = oauth_parse_json_token(data.memory, access_token,
443 sizeof(access_token), &expires_in);
446 curl_easy_cleanup(curl);
452 auth->token = strdup(access_token);
453 if (auth->token == NULL) {
454 ERROR("utils_oauth: strdup failed");
455 auth->valid_until = 0;
458 curl_easy_cleanup(curl);
462 INFO("utils_oauth: OAuth2 access token is valid for %.3fs",
463 CDTIME_T_TO_DOUBLE(expires_in));
464 auth->valid_until = now + expires_in;
467 curl_easy_cleanup(curl);
470 } /* }}} int new_token */
472 static int renew_token(oauth_t *auth) /* {{{ */
474 /* TODO(octo): Make sure that we get a new token 60 seconds or so before the
475 * old one expires. */
476 if (auth->valid_until > cdtime())
479 return new_token(auth);
480 } /* }}} int renew_token */
485 oauth_t *oauth_create(char const *url, char const *iss, char const *scope,
486 char const *aud, EVP_PKEY *key) /* {{{ */
490 if ((url == NULL) || (iss == NULL) || (scope == NULL) || (aud == NULL) ||
494 auth = malloc(sizeof(*auth));
497 memset(auth, 0, sizeof(*auth));
499 auth->url = strdup(url);
500 auth->iss = strdup(iss);
501 auth->scope = strdup(scope);
502 auth->aud = strdup(aud);
504 if ((auth->url == NULL) || (auth->iss == NULL) || (auth->scope == NULL) ||
505 (auth->aud == NULL)) {
513 } /* }}} oauth_t *oauth_create */
515 oauth_t *oauth_create_p12(char const *url, char const *iss, char const *scope,
516 char const *aud, /* {{{ */
517 char const *file, char const *pass) {
518 EVP_PKEY *key = load_p12(file, pass);
520 ERROR("utils_oauth: Failed to load PKCS#12 key from %s", file);
524 return oauth_create(url, iss, scope, aud, key);
525 } /* }}} oauth_t *oauth_create_p12 */
527 oauth_google_t oauth_create_google_json(char const *buffer, char const *scope) {
529 yajl_val root = yajl_tree_parse(buffer, errbuf, sizeof(errbuf));
531 ERROR("utils_oauth: oauth_create_google_json: parse error %s", errbuf);
532 return (oauth_google_t){NULL};
535 yajl_val field_project =
536 yajl_tree_get(root, (char const *[]){"project_id", NULL}, yajl_t_string);
537 if (field_project == NULL) {
538 ERROR("utils_oauth: oauth_create_google_json: project_id field not found");
539 yajl_tree_free(root);
540 return (oauth_google_t){NULL};
542 char const *project_id = YAJL_GET_STRING(field_project);
544 yajl_val field_iss = yajl_tree_get(
545 root, (char const *[]){"client_email", NULL}, yajl_t_string);
546 if (field_iss == NULL) {
548 "utils_oauth: oauth_create_google_json: client_email field not found");
549 yajl_tree_free(root);
550 return (oauth_google_t){NULL};
553 yajl_val field_token_uri =
554 yajl_tree_get(root, (char const *[]){"token_uri", NULL}, yajl_t_string);
555 char const *token_uri = (field_token_uri != NULL)
556 ? YAJL_GET_STRING(field_token_uri)
559 yajl_val field_priv_key =
560 yajl_tree_get(root, (char const *[]){"private_key", NULL}, yajl_t_string);
561 if (field_priv_key == NULL) {
562 ERROR("utils_oauth: oauth_create_google_json: private_key field not found");
563 yajl_tree_free(root);
564 return (oauth_google_t){NULL};
567 BIO *bp = BIO_new_mem_buf(YAJL_GET_STRING(field_priv_key), -1);
568 EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL);
571 ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
573 "utils_oauth: oauth_create_google_json: parsing private key failed: %s",
576 yajl_tree_free(root);
577 return (oauth_google_t){NULL};
582 oauth_t *oauth = oauth_create(token_uri, YAJL_GET_STRING(field_iss), scope,
585 yajl_tree_free(root);
586 return (oauth_google_t){NULL};
589 oauth_google_t ret = {
590 .project_id = strdup(project_id), .oauth = oauth,
593 yajl_tree_free(root);
595 } /* oauth_google_t oauth_create_google_json */
597 oauth_google_t oauth_create_google_file(char const *path,
598 char const *scope) { /* {{{ */
599 int fd = open(path, O_RDONLY);
601 return (oauth_google_t){NULL};
603 struct stat st = {0};
604 if (fstat(fd, &st) != 0) {
606 return (oauth_google_t){NULL};
609 size_t buf_size = (size_t)st.st_size;
610 char *buf = calloc(1, buf_size + 1);
613 return (oauth_google_t){NULL};
616 if (sread(fd, buf, buf_size) != 0) {
619 return (oauth_google_t){NULL};
624 oauth_google_t ret = oauth_create_google_json(buf, scope);
628 } /* }}} oauth_google_t oauth_create_google_file */
630 /* oauth_create_google_default checks for JSON credentials in well-known
631 * positions, similar to gcloud and other tools. */
632 oauth_google_t oauth_create_google_default(char const *scope) {
633 char const *app_creds;
634 if ((app_creds = getenv("GOOGLE_APPLICATION_CREDENTIALS")) != NULL) {
635 oauth_google_t ret = oauth_create_google_file(app_creds, scope);
636 if (ret.oauth == NULL) {
637 ERROR("The environment variable GOOGLE_APPLICATION_CREDENTIALS is set to "
638 "\"%s\" but that file could not be read.",
646 if ((home = getenv("HOME")) != NULL) {
648 snprintf(path, sizeof(path),
649 "%s/.config/gcloud/application_default_credentials.json", home);
651 oauth_google_t ret = oauth_create_google_file(path, scope);
652 if (ret.oauth != NULL) {
657 return (oauth_google_t){NULL};
658 } /* }}} oauth_google_t oauth_create_google_default */
660 void oauth_destroy(oauth_t *auth) /* {{{ */
670 if (auth->key != NULL) {
671 EVP_PKEY_free(auth->key);
676 } /* }}} void oauth_destroy */
678 int oauth_access_token(oauth_t *auth, char *buffer,
679 size_t buffer_size) /* {{{ */
686 status = renew_token(auth);
689 assert(auth->token != NULL);
691 sstrncpy(buffer, auth->token, buffer_size);
693 } /* }}} int oauth_access_token */