4b31056dc30e5730f32ff573b2d2998526b75658
[collectd.git] / src / utils / oauth / 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 "plugin.h"
26 #include "utils/common/common.h"
27 #include "utils/oauth/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 /* Base64-encodes "s" and stores the result in buffer.
108  * Returns zero on success, non-zero otherwise. */
109 static int base64_encode_n(char const *s, size_t s_size, /* {{{ */
110                            char *buffer, size_t buffer_size) {
111   BIO *b64;
112   BUF_MEM *bptr;
113   int status;
114   size_t i;
115
116   /* Set up the memory-base64 chain */
117   b64 = BIO_new(BIO_f_base64());
118   BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
119   b64 = BIO_push(b64, BIO_new(BIO_s_mem()));
120
121   /* Write data to the chain */
122   BIO_write(b64, (void const *)s, s_size);
123   status = BIO_flush(b64);
124   if (status != 1) {
125     ERROR("utils_oauth: base64_encode: BIO_flush() failed.");
126     BIO_free_all(b64);
127     return -1;
128   }
129
130   /* Never fails */
131   BIO_get_mem_ptr(b64, &bptr);
132
133   if (buffer_size <= bptr->length) {
134     ERROR("utils_oauth: base64_encode: Buffer too small.");
135     BIO_free_all(b64);
136     return -1;
137   }
138
139   /* Copy data to buffer. */
140   memcpy(buffer, bptr->data, bptr->length);
141   buffer[bptr->length] = 0;
142
143   /* replace + with -, / with _ and remove padding = at the end */
144   for (i = 0; i < bptr->length; i++) {
145     if (buffer[i] == '+') {
146       buffer[i] = '-';
147     } else if (buffer[i] == '/') {
148       buffer[i] = '_';
149     } else if (buffer[i] == '=') {
150       buffer[i] = 0;
151     }
152   }
153
154   BIO_free_all(b64);
155   return 0;
156 } /* }}} int base64_encode_n */
157
158 /* Base64-encodes "s" and stores the result in buffer.
159  * Returns zero on success, non-zero otherwise. */
160 static int base64_encode(char const *s, /* {{{ */
161                          char *buffer, size_t buffer_size) {
162   return base64_encode_n(s, strlen(s), buffer, buffer_size);
163 } /* }}} int base64_encode */
164
165 /* get_header returns the base64 encoded OAuth header. */
166 static int get_header(char *buffer, size_t buffer_size) /* {{{ */
167 {
168   char header[] = OAUTH_HEADER;
169
170   return base64_encode(header, buffer, buffer_size);
171 } /* }}} int get_header */
172
173 /* get_claim constructs an OAuth claim and returns it as base64 encoded string.
174  */
175 static int get_claim(oauth_t *auth, char *buffer, size_t buffer_size) /* {{{ */
176 {
177   char claim[buffer_size];
178   cdtime_t exp;
179   cdtime_t iat;
180   int status;
181
182   iat = cdtime();
183   exp = iat + OAUTH_EXPIRATION_TIME;
184
185   /* create the claim set */
186   status =
187       snprintf(claim, sizeof(claim), OAUTH_CLAIM_FORMAT, auth->iss, auth->scope,
188                auth->aud, (unsigned long)CDTIME_T_TO_TIME_T(exp),
189                (unsigned long)CDTIME_T_TO_TIME_T(iat));
190   if (status < 1)
191     return -1;
192   else if ((size_t)status >= sizeof(claim))
193     return ENOMEM;
194
195   DEBUG("utils_oauth: get_claim() = %s", claim);
196
197   return base64_encode(claim, buffer, buffer_size);
198 } /* }}} int get_claim */
199
200 /* get_signature signs header and claim with pkey and returns the signature in
201  * buffer. */
202 static int get_signature(char *buffer, size_t buffer_size, /* {{{ */
203                          char const *header, char const *claim,
204                          EVP_PKEY *pkey) {
205   char payload[buffer_size];
206   size_t payload_len;
207   char signature[buffer_size];
208   unsigned int signature_size;
209   int status;
210
211   /* Make the string to sign */
212   payload_len = snprintf(payload, sizeof(payload), "%s.%s", header, claim);
213   if (payload_len < 1) {
214     return -1;
215   } else if (payload_len >= sizeof(payload)) {
216     return ENOMEM;
217   }
218
219   /* Create the signature */
220   signature_size = EVP_PKEY_size(pkey);
221   if (signature_size > sizeof(signature)) {
222     ERROR("utils_oauth: Signature is too large (%u bytes).", signature_size);
223     return -1;
224   }
225
226   EVP_MD_CTX *ctx = EVP_MD_CTX_new();
227
228   /* EVP_SignInit(3SSL) claims this is a void function, but in fact it returns
229    * an int. We're not going to rely on this, though. */
230   EVP_SignInit(ctx, EVP_sha256());
231
232   status = EVP_SignUpdate(ctx, payload, payload_len);
233   if (status != 1) {
234     char errbuf[1024];
235     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
236     ERROR("utils_oauth: EVP_SignUpdate failed: %s", errbuf);
237
238     EVP_MD_CTX_free(ctx);
239     return -1;
240   }
241
242   status =
243       EVP_SignFinal(ctx, (unsigned char *)signature, &signature_size, pkey);
244   if (status != 1) {
245     char errbuf[1024];
246     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
247     ERROR("utils_oauth: EVP_SignFinal failed: %s", errbuf);
248
249     EVP_MD_CTX_free(ctx);
250     return -1;
251   }
252
253   EVP_MD_CTX_free(ctx);
254
255   return base64_encode_n(signature, (size_t)signature_size, buffer,
256                          buffer_size);
257 } /* }}} int get_signature */
258
259 static int get_assertion(oauth_t *auth, char *buffer,
260                          size_t buffer_size) /* {{{ */
261 {
262   char header[buffer_size];
263   char claim[buffer_size];
264   char signature[buffer_size];
265   int status;
266
267   status = get_header(header, sizeof(header));
268   if (status != 0)
269     return -1;
270
271   status = get_claim(auth, claim, sizeof(claim));
272   if (status != 0)
273     return -1;
274
275   status =
276       get_signature(signature, sizeof(signature), header, claim, auth->key);
277   if (status != 0)
278     return -1;
279
280   status = snprintf(buffer, buffer_size, "%s.%s.%s", header, claim, signature);
281   if (status < 1)
282     return -1;
283   else if ((size_t)status >= buffer_size)
284     return ENOMEM;
285
286   return 0;
287 } /* }}} int get_assertion */
288
289 int oauth_parse_json_token(char const *json, /* {{{ */
290                            char *out_access_token, size_t access_token_size,
291                            cdtime_t *expires_in) {
292   time_t expire_in_seconds = 0;
293   yajl_val root;
294   yajl_val token_val;
295   yajl_val expire_val;
296   char errbuf[1024];
297   const char *token_path[] = {"access_token", NULL};
298   const char *expire_path[] = {"expires_in", NULL};
299
300   root = yajl_tree_parse(json, errbuf, sizeof(errbuf));
301   if (root == NULL) {
302     ERROR("utils_oauth: oauth_parse_json_token: parse error %s", errbuf);
303     return -1;
304   }
305
306   token_val = yajl_tree_get(root, token_path, yajl_t_string);
307   if (token_val == NULL) {
308     ERROR("utils_oauth: oauth_parse_json_token: access token field not found");
309     yajl_tree_free(root);
310     return -1;
311   }
312   sstrncpy(out_access_token, YAJL_GET_STRING(token_val), access_token_size);
313
314   expire_val = yajl_tree_get(root, expire_path, yajl_t_number);
315   if (expire_val == NULL) {
316     ERROR("utils_oauth: oauth_parse_json_token: expire field found");
317     yajl_tree_free(root);
318     return -1;
319   }
320   expire_in_seconds = (time_t)YAJL_GET_INTEGER(expire_val);
321   DEBUG("oauth_parse_json_token: expires_in %lu",
322         (unsigned long)expire_in_seconds);
323
324   *expires_in = TIME_T_TO_CDTIME_T(expire_in_seconds);
325   yajl_tree_free(root);
326   return 0;
327 } /* }}} int oauth_parse_json_token */
328
329 static int new_token(oauth_t *auth) /* {{{ */
330 {
331   CURL *curl;
332   char assertion[1024];
333   char post_data[1024];
334   memory_t data;
335   char access_token[256];
336   cdtime_t expires_in;
337   cdtime_t now;
338   char curl_errbuf[CURL_ERROR_SIZE];
339   int status = 0;
340
341   data.size = 0;
342   data.memory = NULL;
343
344   now = cdtime();
345
346   status = get_assertion(auth, assertion, sizeof(assertion));
347   if (status != 0) {
348     ERROR("utils_oauth: Failed to get token using service account %s.",
349           auth->iss);
350     return -1;
351   }
352
353   snprintf(post_data, sizeof(post_data), "grant_type=%s&assertion=%s",
354            OAUTH_GRANT_TYPE, assertion);
355
356   curl = curl_easy_init();
357   if (curl == NULL) {
358     ERROR("utils_oauth: curl_easy_init failed.");
359     return -1;
360   }
361
362   curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
363   curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
364   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_memory);
365   curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
366   curl_easy_setopt(curl, CURLOPT_POST, 1L);
367   curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
368   curl_easy_setopt(curl, CURLOPT_URL, auth->url);
369
370   status = curl_easy_perform(curl);
371   if (status != CURLE_OK) {
372     ERROR("utils_oauth: curl_easy_perform failed with status %i: %s", status,
373           curl_errbuf);
374
375     sfree(data.memory);
376     curl_easy_cleanup(curl);
377
378     return -1;
379   } else {
380     long http_code = 0;
381
382     curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
383     if ((http_code < 200) || (http_code >= 300)) {
384       ERROR("utils_oauth: POST request to %s failed: HTTP error %ld", auth->url,
385             http_code);
386       if (data.memory != NULL)
387         INFO("utils_oauth: Server replied: %s", data.memory);
388
389       sfree(data.memory);
390       curl_easy_cleanup(curl);
391
392       return -1;
393     }
394   }
395
396   status = oauth_parse_json_token(data.memory, access_token,
397                                   sizeof(access_token), &expires_in);
398   if (status != 0) {
399     sfree(data.memory);
400     curl_easy_cleanup(curl);
401
402     return -1;
403   }
404
405   sfree(auth->token);
406   auth->token = strdup(access_token);
407   if (auth->token == NULL) {
408     ERROR("utils_oauth: strdup failed");
409     auth->valid_until = 0;
410
411     sfree(data.memory);
412     curl_easy_cleanup(curl);
413     return -1;
414   }
415
416   INFO("utils_oauth: OAuth2 access token is valid for %.3fs",
417        CDTIME_T_TO_DOUBLE(expires_in));
418   auth->valid_until = now + expires_in;
419
420   sfree(data.memory);
421   curl_easy_cleanup(curl);
422
423   return 0;
424 } /* }}} int new_token */
425
426 static int renew_token(oauth_t *auth) /* {{{ */
427 {
428   /* Renew OAuth token 30 seconds *before* it expires. */
429   cdtime_t const slack = TIME_T_TO_CDTIME_T(30);
430
431   if (auth->valid_until > (cdtime() + slack))
432     return 0;
433
434   return new_token(auth);
435 } /* }}} int renew_token */
436
437 static oauth_t *oauth_create(char const *url, char const *iss,
438                              char const *scope, char const *aud,
439                              EVP_PKEY *key) /* {{{ */
440 {
441   oauth_t *auth;
442
443   if ((url == NULL) || (iss == NULL) || (scope == NULL) || (aud == NULL) ||
444       (key == NULL))
445     return NULL;
446
447   auth = malloc(sizeof(*auth));
448   if (auth == NULL)
449     return NULL;
450   memset(auth, 0, sizeof(*auth));
451
452   auth->url = strdup(url);
453   auth->iss = strdup(iss);
454   auth->scope = strdup(scope);
455   auth->aud = strdup(aud);
456
457   if ((auth->url == NULL) || (auth->iss == NULL) || (auth->scope == NULL) ||
458       (auth->aud == NULL)) {
459     oauth_destroy(auth);
460     return NULL;
461   }
462
463   auth->key = key;
464
465   return auth;
466 } /* }}} oauth_t *oauth_create */
467
468 /*
469  * Public
470  */
471 oauth_google_t oauth_create_google_json(char const *buffer, char const *scope) {
472   char errbuf[1024];
473   yajl_val root = yajl_tree_parse(buffer, errbuf, sizeof(errbuf));
474   if (root == NULL) {
475     ERROR("utils_oauth: oauth_create_google_json: parse error %s", errbuf);
476     return (oauth_google_t){NULL};
477   }
478
479   yajl_val field_project =
480       yajl_tree_get(root, (char const *[]){"project_id", NULL}, yajl_t_string);
481   if (field_project == NULL) {
482     ERROR("utils_oauth: oauth_create_google_json: project_id field not found");
483     yajl_tree_free(root);
484     return (oauth_google_t){NULL};
485   }
486   char const *project_id = YAJL_GET_STRING(field_project);
487
488   yajl_val field_iss = yajl_tree_get(
489       root, (char const *[]){"client_email", NULL}, yajl_t_string);
490   if (field_iss == NULL) {
491     ERROR(
492         "utils_oauth: oauth_create_google_json: client_email field not found");
493     yajl_tree_free(root);
494     return (oauth_google_t){NULL};
495   }
496
497   yajl_val field_token_uri =
498       yajl_tree_get(root, (char const *[]){"token_uri", NULL}, yajl_t_string);
499   char const *token_uri = (field_token_uri != NULL)
500                               ? YAJL_GET_STRING(field_token_uri)
501                               : GOOGLE_TOKEN_URL;
502
503   yajl_val field_priv_key =
504       yajl_tree_get(root, (char const *[]){"private_key", NULL}, yajl_t_string);
505   if (field_priv_key == NULL) {
506     ERROR("utils_oauth: oauth_create_google_json: private_key field not found");
507     yajl_tree_free(root);
508     return (oauth_google_t){NULL};
509   }
510
511   BIO *bp = BIO_new_mem_buf(YAJL_GET_STRING(field_priv_key), -1);
512   EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL);
513   if (pkey == NULL) {
514     char errbuf[1024];
515     ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
516     ERROR(
517         "utils_oauth: oauth_create_google_json: parsing private key failed: %s",
518         errbuf);
519     BIO_free(bp);
520     yajl_tree_free(root);
521     return (oauth_google_t){NULL};
522   }
523
524   BIO_free(bp);
525
526   oauth_t *oauth = oauth_create(token_uri, YAJL_GET_STRING(field_iss), scope,
527                                 token_uri, pkey);
528   if (oauth == NULL) {
529     yajl_tree_free(root);
530     return (oauth_google_t){NULL};
531   }
532
533   oauth_google_t ret = {
534       .project_id = strdup(project_id), .oauth = oauth,
535   };
536
537   yajl_tree_free(root);
538   return ret;
539 } /* oauth_google_t oauth_create_google_json */
540
541 oauth_google_t oauth_create_google_file(char const *path,
542                                         char const *scope) { /* {{{ */
543   int fd = open(path, O_RDONLY);
544   if (fd == -1)
545     return (oauth_google_t){NULL};
546
547   struct stat st = {0};
548   if (fstat(fd, &st) != 0) {
549     close(fd);
550     return (oauth_google_t){NULL};
551   }
552
553   size_t buf_size = (size_t)st.st_size;
554   char *buf = calloc(1, buf_size + 1);
555   if (buf == NULL) {
556     close(fd);
557     return (oauth_google_t){NULL};
558   }
559
560   if (sread(fd, buf, buf_size) != 0) {
561     free(buf);
562     close(fd);
563     return (oauth_google_t){NULL};
564   }
565   close(fd);
566   buf[buf_size] = 0;
567
568   oauth_google_t ret = oauth_create_google_json(buf, scope);
569
570   free(buf);
571   return ret;
572 } /* }}} oauth_google_t oauth_create_google_file */
573
574 /* oauth_create_google_default checks for JSON credentials in well-known
575  * positions, similar to gcloud and other tools. */
576 oauth_google_t oauth_create_google_default(char const *scope) {
577   char const *app_creds;
578   if ((app_creds = getenv("GOOGLE_APPLICATION_CREDENTIALS")) != NULL) {
579     oauth_google_t ret = oauth_create_google_file(app_creds, scope);
580     if (ret.oauth == NULL) {
581       ERROR("The environment variable GOOGLE_APPLICATION_CREDENTIALS is set to "
582             "\"%s\" but that file could not be read.",
583             app_creds);
584     } else {
585       return ret;
586     }
587   }
588
589   char const *home;
590   if ((home = getenv("HOME")) != NULL) {
591     char path[PATH_MAX];
592     snprintf(path, sizeof(path),
593              "%s/.config/gcloud/application_default_credentials.json", home);
594
595     oauth_google_t ret = oauth_create_google_file(path, scope);
596     if (ret.oauth != NULL) {
597       return ret;
598     }
599   }
600
601   return (oauth_google_t){NULL};
602 } /* }}} oauth_google_t oauth_create_google_default */
603
604 void oauth_destroy(oauth_t *auth) /* {{{ */
605 {
606   if (auth == NULL)
607     return;
608
609   sfree(auth->url);
610   sfree(auth->iss);
611   sfree(auth->scope);
612   sfree(auth->aud);
613
614   if (auth->key != NULL) {
615     EVP_PKEY_free(auth->key);
616     auth->key = NULL;
617   }
618
619   sfree(auth);
620 } /* }}} void oauth_destroy */
621
622 int oauth_access_token(oauth_t *auth, char *buffer,
623                        size_t buffer_size) /* {{{ */
624 {
625   int status;
626
627   if (auth == NULL)
628     return EINVAL;
629
630   status = renew_token(auth);
631   if (status != 0)
632     return status;
633   assert(auth->token != NULL);
634
635   sstrncpy(buffer, auth->token, buffer_size);
636   return 0;
637 } /* }}} int oauth_access_token */