src/utils_oauth.[ch]: Add utility for OAuth authentication.
[collectd.git] / src / utils_oauth.c
1 /**
2  * collectd - src/utils_oauth.c
3  * ISC license
4  *
5  * Copyright (C) 2017  Florian Forster
6  *
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.
10  *
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.
18  *
19  * Authors:
20  *   Florian Forster <octo at collectd.org>
21  **/
22
23 #include "collectd.h"
24
25 #include "common.h"
26 #include "plugin.h"
27 #include "utils_oauth.h"
28
29 #include <curl/curl.h>
30
31 #include <yajl/yajl_tree.h>
32 #include <yajl/yajl_version.h>
33
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>
39
40 /*
41  * Private variables
42  */
43 #define GOOGLE_TOKEN_URL "https://accounts.google.com/o/oauth2/token"
44
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
47  */
48 #define MAX_BUFFER_SIZE 65536
49 #define MAX_ENCODE_SIZE 2048
50
51 struct oauth_s {
52   char *url;
53   char *iss;
54   char *aud;
55   char *scope;
56
57   EVP_PKEY *key;
58
59   char *token;
60   cdtime_t valid_until;
61 };
62
63 struct memory_s {
64   char *memory;
65   size_t size;
66 };
67 typedef struct memory_s memory_t;
68
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\"}"
72
73 static const char OAUTH_CLAIM_FORMAT[] = "{"
74                                          "\"iss\":\"%s\","
75                                          "\"scope\":\"%s\","
76                                          "\"aud\":\"%s\","
77                                          "\"exp\":%lu,"
78                                          "\"iat\":%lu"
79                                          "}";
80
81 static size_t write_memory(void *contents, size_t size, size_t nmemb, /* {{{ */
82                            void *userp) {
83   size_t realsize = size * nmemb;
84   memory_t *mem = (memory_t *)userp;
85   char *tmp;
86
87   if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) {
88     ERROR("integer overflow");
89     return 0;
90   }
91
92   tmp = (char *)realloc((void *)mem->memory, mem->size + realsize + 1);
93   if (tmp == NULL) {
94     /* out of memory! */
95     ERROR("write_memory: not enough memory (realloc returned NULL)");
96     return 0;
97   }
98   mem->memory = tmp;
99
100   memcpy(&(mem->memory[mem->size]), contents, realsize);
101   mem->size += realsize;
102   mem->memory[mem->size] = 0;
103
104   return realsize;
105 } /* }}} size_t write_memory */
106
107 static EVP_PKEY *load_p12(/* {{{ */
108                           char const *p12_filename,
109                           char const *p12_passphrase) {
110   FILE *fp;
111   PKCS12 *p12;
112   X509 *cert;
113   STACK_OF(X509) *ca = NULL;
114   EVP_PKEY *pkey = NULL;
115
116   OpenSSL_add_all_algorithms();
117
118   fp = fopen(p12_filename, "rb");
119   if (fp == NULL) {
120     char errbuf[1024];
121     ERROR("utils_oauth: Opening private key %s failed: %s", p12_filename,
122           sstrerror(errno, errbuf, sizeof(errbuf)));
123     return NULL;
124   }
125
126   p12 = d2i_PKCS12_fp(fp, NULL);
127   fclose(fp);
128   if (p12 == NULL) {
129     char errbuf[1024];
130     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
131     ERROR("utils_oauth: Reading private key %s failed: %s", p12_filename,
132           errbuf);
133     return NULL;
134   }
135
136   if (PKCS12_parse(p12, p12_passphrase, &pkey, &cert, &ca) == 0) {
137     char errbuf[1024];
138     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
139     ERROR("utils_oauth: Parsing private key %s failed: %s", p12_filename,
140           errbuf);
141
142     if (cert)
143       X509_free(cert);
144     if (ca)
145       sk_X509_pop_free(ca, X509_free);
146     PKCS12_free(p12);
147     return NULL;
148   }
149
150   return pkey;
151 } /* }}} EVP_PKEY *load_p12 */
152
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) {
157   BIO *b64;
158   BUF_MEM *bptr;
159   int status;
160   size_t i;
161
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()));
166
167   /* Write data to the chain */
168   BIO_write(b64, (void const *)s, s_size);
169   status = BIO_flush(b64);
170   if (status != 1) {
171     ERROR("utils_oauth: base64_encode: BIO_flush() failed.");
172     BIO_free_all(b64);
173     return -1;
174   }
175
176   /* Never fails */
177   BIO_get_mem_ptr(b64, &bptr);
178
179   if (buffer_size <= bptr->length) {
180     ERROR("utils_oauth: base64_encode: Buffer too small.");
181     BIO_free_all(b64);
182     return -1;
183   }
184
185   /* Copy data to buffer. */
186   memcpy(buffer, bptr->data, bptr->length);
187   buffer[bptr->length] = 0;
188
189   /* replace + with -, / with _ and remove padding = at the end */
190   for (i = 0; i < bptr->length; i++) {
191     if (buffer[i] == '+') {
192       buffer[i] = '-';
193     } else if (buffer[i] == '/') {
194       buffer[i] = '_';
195     } else if (buffer[i] == '=') {
196       buffer[i] = 0;
197     }
198   }
199
200   BIO_free_all(b64);
201   return 0;
202 } /* }}} int base64_encode_n */
203
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 */
210
211 /* get_header returns the base64 encoded OAuth header. */
212 static int get_header(char *buffer, size_t buffer_size) /* {{{ */
213 {
214   char header[] = OAUTH_HEADER;
215
216   return base64_encode(header, buffer, buffer_size);
217 } /* }}} int get_header */
218
219 /* get_claim constructs an OAuth claim and returns it as base64 encoded string.
220  */
221 static int get_claim(oauth_t *auth, char *buffer, size_t buffer_size) /* {{{ */
222 {
223   char claim[buffer_size];
224   cdtime_t exp;
225   cdtime_t iat;
226   int status;
227
228   iat = cdtime();
229   exp = iat + OAUTH_EXPIRATION_TIME;
230
231   /* create the claim set */
232   status =
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));
236   if (status < 1)
237     return -1;
238   else if ((size_t)status >= sizeof(claim))
239     return ENOMEM;
240
241   DEBUG("utils_oauth: get_claim() = %s", claim);
242
243   return base64_encode(claim, buffer, buffer_size);
244 } /* }}} int get_claim */
245
246 /* get_signature signs header and claim with pkey and returns the signature in
247  * buffer. */
248 static int get_signature(char *buffer, size_t buffer_size, /* {{{ */
249                          char const *header, char const *claim,
250                          EVP_PKEY *pkey) {
251   char payload[buffer_size];
252   size_t payload_len;
253   char signature[buffer_size];
254   unsigned int signature_size;
255   int status;
256
257   /* Make the string to sign */
258   payload_len = snprintf(payload, sizeof(payload), "%s.%s", header, claim);
259   if (payload_len < 1) {
260     return -1;
261   } else if (payload_len >= sizeof(payload)) {
262     return ENOMEM;
263   }
264
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);
269     return -1;
270   }
271
272   EVP_MD_CTX *ctx = EVP_MD_CTX_new();
273
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());
277
278   status = EVP_SignUpdate(ctx, payload, payload_len);
279   if (status != 1) {
280     char errbuf[1024];
281     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
282     ERROR("utils_oauth: EVP_SignUpdate failed: %s", errbuf);
283
284     EVP_MD_CTX_free(ctx);
285     return -1;
286   }
287
288   status =
289       EVP_SignFinal(ctx, (unsigned char *)signature, &signature_size, pkey);
290   if (status != 1) {
291     char errbuf[1024];
292     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
293     ERROR("utils_oauth: EVP_SignFinal failed: %s", errbuf);
294
295     EVP_MD_CTX_free(ctx);
296     return -1;
297   }
298
299   EVP_MD_CTX_free(ctx);
300
301   return base64_encode_n(signature, (size_t)signature_size, buffer,
302                          buffer_size);
303 } /* }}} int get_signature */
304
305 static int get_assertion(oauth_t *auth, char *buffer,
306                          size_t buffer_size) /* {{{ */
307 {
308   char header[buffer_size];
309   char claim[buffer_size];
310   char signature[buffer_size];
311   int status;
312
313   status = get_header(header, sizeof(header));
314   if (status != 0)
315     return -1;
316
317   status = get_claim(auth, claim, sizeof(claim));
318   if (status != 0)
319     return -1;
320
321   status =
322       get_signature(signature, sizeof(signature), header, claim, auth->key);
323   if (status != 0)
324     return -1;
325
326   status = snprintf(buffer, buffer_size, "%s.%s.%s", header, claim, signature);
327   if (status < 1)
328     return -1;
329   else if (status >= buffer_size)
330     return ENOMEM;
331
332   return 0;
333 } /* }}} int get_assertion */
334
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;
339   yajl_val root;
340   yajl_val token_val;
341   yajl_val expire_val;
342   char errbuf[1024];
343   const char *token_path[] = {"access_token", NULL};
344   const char *expire_path[] = {"expires_in", NULL};
345
346   root = yajl_tree_parse(json, errbuf, sizeof(errbuf));
347   if (root == NULL) {
348     ERROR("utils_oauth: oauth_parse_json_token: parse error %s", errbuf);
349     return -1;
350   }
351
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);
356     return -1;
357   }
358   sstrncpy(out_access_token, YAJL_GET_STRING(token_val), access_token_size);
359
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);
364     return -1;
365   }
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);
369
370   *expires_in = TIME_T_TO_CDTIME_T(expire_in_seconds);
371   yajl_tree_free(root);
372   return 0;
373 } /* }}} int oauth_parse_json_token */
374
375 static int new_token(oauth_t *auth) /* {{{ */
376 {
377   CURL *curl;
378   char assertion[1024];
379   char post_data[1024];
380   memory_t data;
381   char access_token[256];
382   cdtime_t expires_in;
383   cdtime_t now;
384   char curl_errbuf[CURL_ERROR_SIZE];
385   int status = 0;
386
387   data.size = 0;
388   data.memory = NULL;
389
390   now = cdtime();
391
392   status = get_assertion(auth, assertion, sizeof(assertion));
393   if (status != 0) {
394     ERROR("utils_oauth: Failed to get token using service account %s.",
395           auth->iss);
396     return -1;
397   }
398
399   snprintf(post_data, sizeof(post_data), "grant_type=%s&assertion=%s",
400            OAUTH_GRANT_TYPE, assertion);
401
402   curl = curl_easy_init();
403   if (curl == NULL) {
404     ERROR("utils_oauth: curl_easy_init failed.");
405     return -1;
406   }
407
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);
415
416   status = curl_easy_perform(curl);
417   if (status != CURLE_OK) {
418     ERROR("utils_oauth: curl_easy_perform failed with status %i: %s", status,
419           curl_errbuf);
420
421     sfree(data.memory);
422     curl_easy_cleanup(curl);
423
424     return -1;
425   } else {
426     long http_code = 0;
427
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,
431             http_code);
432       if (data.memory != NULL)
433         INFO("utils_oauth: Server replied: %s", data.memory);
434
435       sfree(data.memory);
436       curl_easy_cleanup(curl);
437
438       return -1;
439     }
440   }
441
442   status = oauth_parse_json_token(data.memory, access_token,
443                                   sizeof(access_token), &expires_in);
444   if (status != 0) {
445     sfree(data.memory);
446     curl_easy_cleanup(curl);
447
448     return -1;
449   }
450
451   sfree(auth->token);
452   auth->token = strdup(access_token);
453   if (auth->token == NULL) {
454     ERROR("utils_oauth: strdup failed");
455     auth->valid_until = 0;
456
457     sfree(data.memory);
458     curl_easy_cleanup(curl);
459     return -1;
460   }
461
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;
465
466   sfree(data.memory);
467   curl_easy_cleanup(curl);
468
469   return 0;
470 } /* }}} int new_token */
471
472 static int renew_token(oauth_t *auth) /* {{{ */
473 {
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())
477     return 0;
478
479   return new_token(auth);
480 } /* }}} int renew_token */
481
482 /*
483  * Public
484  */
485 oauth_t *oauth_create(char const *url, char const *iss, char const *scope,
486                       char const *aud, EVP_PKEY *key) /* {{{ */
487 {
488   oauth_t *auth;
489
490   if ((url == NULL) || (iss == NULL) || (scope == NULL) || (aud == NULL) ||
491       (key == NULL))
492     return NULL;
493
494   auth = malloc(sizeof(*auth));
495   if (auth == NULL)
496     return NULL;
497   memset(auth, 0, sizeof(*auth));
498
499   auth->url = strdup(url);
500   auth->iss = strdup(iss);
501   auth->scope = strdup(scope);
502   auth->aud = strdup(aud);
503
504   if ((auth->url == NULL) || (auth->iss == NULL) || (auth->scope == NULL) ||
505       (auth->aud == NULL)) {
506     oauth_destroy(auth);
507     return NULL;
508   }
509
510   auth->key = key;
511
512   return auth;
513 } /* }}} oauth_t *oauth_create */
514
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);
519   if (key == NULL) {
520     ERROR("utils_oauth: Failed to load PKCS#12 key from %s", file);
521     return NULL;
522   }
523
524   return oauth_create(url, iss, scope, aud, key);
525 } /* }}} oauth_t *oauth_create_p12 */
526
527 oauth_google_t oauth_create_google_json(char const *buffer, char const *scope) {
528   char errbuf[1024];
529   yajl_val root = yajl_tree_parse(buffer, errbuf, sizeof(errbuf));
530   if (root == NULL) {
531     ERROR("utils_oauth: oauth_create_google_json: parse error %s", errbuf);
532     return (oauth_google_t){NULL};
533   }
534
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};
541   }
542   char const *project_id = YAJL_GET_STRING(field_project);
543
544   yajl_val field_iss = yajl_tree_get(
545       root, (char const *[]){"client_email", NULL}, yajl_t_string);
546   if (field_iss == NULL) {
547     ERROR(
548         "utils_oauth: oauth_create_google_json: client_email field not found");
549     yajl_tree_free(root);
550     return (oauth_google_t){NULL};
551   }
552
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)
557                               : GOOGLE_TOKEN_URL;
558
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};
565   }
566
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);
569   if (pkey == NULL) {
570     char errbuf[1024];
571     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
572     ERROR(
573         "utils_oauth: oauth_create_google_json: parsing private key failed: %s",
574         errbuf);
575     BIO_free(bp);
576     yajl_tree_free(root);
577     return (oauth_google_t){NULL};
578   }
579
580   BIO_free(bp);
581
582   oauth_t *oauth = oauth_create(token_uri, YAJL_GET_STRING(field_iss), scope,
583                                 token_uri, pkey);
584   if (oauth == NULL) {
585     yajl_tree_free(root);
586     return (oauth_google_t){NULL};
587   }
588
589   oauth_google_t ret = {
590       .project_id = strdup(project_id), .oauth = oauth,
591   };
592
593   yajl_tree_free(root);
594   return ret;
595 } /* oauth_google_t oauth_create_google_json */
596
597 oauth_google_t oauth_create_google_file(char const *path,
598                                         char const *scope) { /* {{{ */
599   int fd = open(path, O_RDONLY);
600   if (fd == -1)
601     return (oauth_google_t){NULL};
602
603   struct stat st = {0};
604   if (fstat(fd, &st) != 0) {
605     close(fd);
606     return (oauth_google_t){NULL};
607   }
608
609   size_t buf_size = (size_t)st.st_size;
610   char *buf = calloc(1, buf_size + 1);
611   if (buf == NULL) {
612     close(fd);
613     return (oauth_google_t){NULL};
614   }
615
616   if (sread(fd, buf, buf_size) != 0) {
617     free(buf);
618     close(fd);
619     return (oauth_google_t){NULL};
620   }
621   close(fd);
622   buf[buf_size] = 0;
623
624   oauth_google_t ret = oauth_create_google_json(buf, scope);
625
626   free(buf);
627   return ret;
628 } /* }}} oauth_google_t oauth_create_google_file */
629
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.",
639             app_creds);
640     } else {
641       return ret;
642     }
643   }
644
645   char const *home;
646   if ((home = getenv("HOME")) != NULL) {
647     char path[PATH_MAX];
648     snprintf(path, sizeof(path),
649              "%s/.config/gcloud/application_default_credentials.json", home);
650
651     oauth_google_t ret = oauth_create_google_file(path, scope);
652     if (ret.oauth != NULL) {
653       return ret;
654     }
655   }
656
657   return (oauth_google_t){NULL};
658 } /* }}} oauth_google_t oauth_create_google_default */
659
660 void oauth_destroy(oauth_t *auth) /* {{{ */
661 {
662   if (auth == NULL)
663     return;
664
665   sfree(auth->url);
666   sfree(auth->iss);
667   sfree(auth->scope);
668   sfree(auth->aud);
669
670   if (auth->key != NULL) {
671     EVP_PKEY_free(auth->key);
672     auth->key = NULL;
673   }
674
675   sfree(auth);
676 } /* }}} void oauth_destroy */
677
678 int oauth_access_token(oauth_t *auth, char *buffer,
679                        size_t buffer_size) /* {{{ */
680 {
681   int status;
682
683   if (auth == NULL)
684     return EINVAL;
685
686   status = renew_token(auth);
687   if (status != 0)
688     return status;
689   assert(auth->token != NULL);
690
691   sstrncpy(buffer, auth->token, buffer_size);
692   return 0;
693 } /* }}} int oauth_access_token */