diff options
Diffstat (limited to '')
-rw-r--r-- | src/oidc_child/oidc_child.c | 636 | ||||
-rw-r--r-- | src/oidc_child/oidc_child_curl.c | 524 | ||||
-rw-r--r-- | src/oidc_child/oidc_child_json.c | 511 | ||||
-rw-r--r-- | src/oidc_child/oidc_child_util.h | 99 |
4 files changed, 1770 insertions, 0 deletions
diff --git a/src/oidc_child/oidc_child.c b/src/oidc_child/oidc_child.c new file mode 100644 index 0000000..7758cdc --- /dev/null +++ b/src/oidc_child/oidc_child.c @@ -0,0 +1,636 @@ +/* + SSSD + + Helper child for OIDC and OAuth 2.0 Device Authorization Grant + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2022 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <popt.h> + +#include "oidc_child/oidc_child_util.h" + +#include "util/util.h" +#include "util/atomic_io.h" + +#define IN_BUF_SIZE 4096 +static errno_t read_from_stdin(TALLOC_CTX *mem_ctx, char **out) +{ + uint8_t buf[IN_BUF_SIZE]; + ssize_t len; + errno_t ret; + char *str; + + errno = 0; + len = sss_atomic_read_s(STDIN_FILENO, buf, IN_BUF_SIZE); + if (len == -1) { + ret = errno; + ret = (ret == 0) ? EINVAL: ret; + DEBUG(SSSDBG_CRIT_FAILURE, + "read failed [%d][%s].\n", ret, strerror(ret)); + return ret; + } + + if (len == 0 || *buf == '\0') { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing device code\n"); + return EINVAL; + } + + str = talloc_strndup(mem_ctx, (char *) buf, len); + sss_erase_mem_securely(buf, IN_BUF_SIZE); + if (str == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strndup failed.\n"); + return ENOMEM; + } + talloc_set_destructor((void *) str, sss_erase_talloc_mem_securely); + + if (strlen(str) != len) { + DEBUG(SSSDBG_CRIT_FAILURE, "Input contains additional data.\n"); + talloc_free(str); + return EINVAL; + } + + *out = str; + + return EOK; +} + +static errno_t read_device_code_from_stdin(struct devicecode_ctx *dc_ctx, + const char **out) +{ + char *str; + errno_t ret; + char *sep; + + ret = read_from_stdin(dc_ctx, &str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "read_from_stdin failed.\n"); + return ret; + } + + if (out != NULL) { + /* expect the client secret in the first line */ + sep = strchr(str, '\n'); + if (sep == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Format error, expecting client secret and JSON data.\n"); + talloc_free(str); + return EINVAL; + } + *sep = '\0'; + *out = str; + sep++; + } else { + sep = str; + } + + clean_http_data(dc_ctx); + dc_ctx->http_data = talloc_strdup(dc_ctx, sep); + + DEBUG(SSSDBG_TRACE_ALL, "JSON device code: [%s].\n", dc_ctx->http_data); + + return EOK; +} + +static errno_t read_client_secret_from_stdin(struct devicecode_ctx *dc_ctx, + const char **out) +{ + char *str; + errno_t ret; + + ret = read_from_stdin(dc_ctx, &str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "read_from_stdin failed.\n"); + return ret; + } + + *out = str; + + DEBUG(SSSDBG_TRACE_ALL, "Client secret: [%s].\n", *out); + + return EOK; +} + +static errno_t set_endpoints(struct devicecode_ctx *dc_ctx, + const char *device_auth_endpoint, + const char *token_endpoint, + const char *userinfo_endpoint, + const char *jwks_uri, + const char *scope) +{ + int ret; + + dc_ctx->device_authorization_endpoint = talloc_strdup(dc_ctx, + device_auth_endpoint); + if (dc_ctx->device_authorization_endpoint == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing device_authorization_endpoint.\n"); + ret = EINVAL; + goto done; + } + dc_ctx->token_endpoint = talloc_strdup(dc_ctx, token_endpoint); + if (dc_ctx->token_endpoint == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing token_endpoint.\n"); + ret = EINVAL; + goto done; + } + dc_ctx->userinfo_endpoint = talloc_strdup(dc_ctx, userinfo_endpoint); + if (dc_ctx->userinfo_endpoint == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing userinfo_endpoint.\n"); + ret = EINVAL; + goto done; + } + + if (jwks_uri != NULL && *jwks_uri != '\0') { + dc_ctx->jwks_uri = talloc_strdup(dc_ctx, jwks_uri); + if (dc_ctx->jwks_uri == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to copy jwks_uri.\n"); + ret = ENOMEM; + goto done; + } + } + + if (scope != NULL && *scope != '\0') { + dc_ctx->scope = url_encode_string(dc_ctx, scope); + if (dc_ctx->scope == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to encode and copy scopes.\n"); + ret = ENOMEM; + goto done; + } + } + + ret = EOK; +done: + return ret; +} + +static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, + bool libcurl_debug, const char *ca_db, + const char *issuer_url, + const char *device_auth_endpoint, + const char *token_endpoint, + const char *userinfo_endpoint, + const char *jwks_uri, const char *scope) +{ + struct devicecode_ctx *dc_ctx = NULL; + int ret; + + dc_ctx = talloc_zero(mem_ctx, struct devicecode_ctx); + if (dc_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to allocate memory for results.\n"); + ret = ENOMEM; + goto done; + } + + ret = init_curl(dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to init libcurl.\n"); + goto done; + } + + dc_ctx->libcurl_debug = libcurl_debug; + + if (ca_db != NULL) { + dc_ctx->ca_db = talloc_strdup(dc_ctx, ca_db); + if (dc_ctx->ca_db == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to copy CA DB path.\n"); + ret = ENOMEM; + goto done; + } + } + + if (issuer_url != NULL) { + ret = get_openid_configuration(dc_ctx, issuer_url); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get openid configuration.\n"); + goto done; + } + + ret = parse_openid_configuration(dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to parse openid configuration.\n"); + goto done; + } + } else if (device_auth_endpoint != NULL && token_endpoint != NULL) { + ret = set_endpoints(dc_ctx, device_auth_endpoint, token_endpoint, + userinfo_endpoint, jwks_uri, scope); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set endpoints.\n"); + goto done; + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing issuer information.\n"); + ret = EINVAL; + goto done; + } + +done: + if (ret != EOK) { + talloc_free(dc_ctx); + dc_ctx = NULL; + } + return dc_ctx; +} + +struct cli_opts { + const char *opt_logger; + const char *issuer_url; + const char *client_id; + const char *device_auth_endpoint; + const char *token_endpoint; + const char *userinfo_endpoint; + const char *jwks_uri; + const char *scope; + const char *client_secret; + bool client_secret_stdin; + const char *ca_db; + const char *user_identifier_attr; + bool libcurl_debug; + bool get_device_code; + bool get_access_token; +}; + +static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) +{ + poptContext pc; + int opt; + errno_t ret; + int debug_fd = -1; + const char *opt_logger = NULL; + bool print_usage = true; + + struct poptOption long_options[] = { + POPT_AUTOHELP + SSSD_DEBUG_OPTS + {"debug-fd", 0, POPT_ARG_INT, &debug_fd, 0, + _("An open file descriptor for the debug logs"), NULL}, + {"get-device-code", 0, POPT_ARG_NONE, NULL, 'a', + _("Get device code and URL"), NULL}, + {"get-access-token", 0, POPT_ARG_NONE, NULL, 'b', + _("Wait for access token"), NULL}, + {"issuer-url", 0, POPT_ARG_STRING, &opts->issuer_url, 0, + _("URL of Issuer IdP"), NULL}, + {"device-auth-endpoint", 0, POPT_ARG_STRING, &opts->device_auth_endpoint, 0, + _("Device authorization endpoint of the IdP"), NULL}, + {"token-endpoint", 0, POPT_ARG_STRING, &opts->token_endpoint, 0, + _("Token endpoint of the IdP"), NULL}, + {"userinfo-endpoint", 0, POPT_ARG_STRING, &opts->userinfo_endpoint, 0, + _("Userinfo endpoint of the IdP"), NULL}, + {"user-identifier-attribute", 0, POPT_ARG_STRING, + &opts->user_identifier_attr, 0, + _("Unique identifier of the user in the userinfo data"), NULL}, + {"jwks-uri", 0, POPT_ARG_STRING, &opts->jwks_uri, 0, + _("JWKS URI of the IdP"), NULL}, + {"scope", 0, POPT_ARG_STRING, &opts->scope, 0, + _("Supported scope of the IdP to get userinfo"), NULL}, + {"client-id", 0, POPT_ARG_STRING, &opts->client_id, 0, _("Client ID"), NULL}, + {"client-secret", 0, POPT_ARG_STRING, &opts->client_secret, 0, + _("Client secret (if needed)"), NULL}, + {"client-secret-stdin", 0, POPT_ARG_NONE, NULL, 's', + _("Read client secret from standard input"), NULL}, + {"ca-db", 0, POPT_ARG_STRING, &opts->ca_db, 0, + _("Path to PEM file with CA certificates"), NULL}, + {"libcurl-debug", 0, POPT_ARG_NONE, NULL, 'c', + _("Enable libcurl debug output"), NULL}, + SSSD_LOGGER_OPTS + POPT_TABLEEND + }; + + /* Set debug level to invalid value so we can decide if -d 0 was used. */ + debug_level = SSSDBG_INVALID; + + umask(SSS_DFL_UMASK); + + ret = EINVAL; /* assume issue with command line arguments */ + + pc = poptGetContext(argv[0], argc, argv, long_options, 0); + while ((opt = poptGetNextOpt(pc)) != -1) { + switch(opt) { + case 'a': + opts->get_device_code = true; + break; + case 'b': + opts->get_access_token = true; + break; + case 'c': + opts->libcurl_debug = true; + break; + case 's': + opts->client_secret_stdin = true; + break; + default: + fprintf(stderr, "\nInvalid option %s: %s\n\n", + poptBadOption(pc, 0), poptStrerror(opt)); + goto done; + } + } + + if (!opts->get_device_code && !opts->get_access_token) { + fprintf(stderr, + "\n--get-device-code or --get-access-token must be given.\n\n"); + goto done; + } + + if (opts->get_device_code && opts->get_access_token) { + fprintf(stderr, + "\n--get-device-code and --get-access-token " + "are mutually exclusive .\n\n"); + goto done; + } + + if ((opts->issuer_url != NULL + && (opts->device_auth_endpoint != NULL + || opts->token_endpoint != NULL)) + || (opts->device_auth_endpoint != NULL && opts->token_endpoint != NULL + && opts->issuer_url != NULL) + || (opts->issuer_url == NULL + && ((opts->device_auth_endpoint != NULL + && opts->token_endpoint == NULL) + || (opts->device_auth_endpoint == NULL + && opts->token_endpoint != NULL))) + || (opts->issuer_url == NULL + && (opts->device_auth_endpoint == NULL + || opts->token_endpoint == NULL))) { + fprintf(stderr, "\n--issuer-url or --device-auth-endpoint " + "together with --token-endpoint are mutually exclusive " + "but one variant must be given.\n\n"); + goto done; + } + + if (opts->client_id == NULL) { + fprintf(stderr, "\n--client-id must be given.\n\n"); + goto done; + } + + if (opts->client_secret != NULL && opts->client_secret_stdin) { + fprintf(stderr, "\n--client-secret and --client-secret-stdin are " + "mutually exclusive.\n\n"); + goto done; + } + + poptFreeContext(pc); + print_usage = false; + + debug_prg_name = talloc_asprintf(NULL, "oidc_child[%d]", getpid()); + if (debug_prg_name == NULL) { + ERROR("talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + + opts->opt_logger = opt_logger; + + if (debug_fd != -1) { + opts->opt_logger = sss_logger_str[FILES_LOGGER]; + ret = set_debug_file_from_fd(debug_fd); + if (ret != EOK) { + opts->opt_logger = sss_logger_str[STDERR_LOGGER]; + ERROR("set_debug_file_from_fd failed.\n"); + return ret; + } + } + + ret = EOK; + +done: + if (print_usage) { + poptPrintUsage(pc, stderr, 0); + poptFreeContext(pc); + } + + return ret; +} + +void trace_device_code(struct devicecode_ctx *dc_ctx, bool get_device_code) +{ + if (!DEBUG_IS_SET(SSSDBG_TRACE_ALL)) { + return; + } + + if (get_device_code) { + DEBUG(SSSDBG_TRACE_ALL, "user_code: [%s].\n", dc_ctx->user_code); + DEBUG(SSSDBG_TRACE_ALL, "verification_uri: [%s].\n", + dc_ctx->verification_uri); + DEBUG(SSSDBG_TRACE_ALL, "verification_uri_complete: [%s].\n", + dc_ctx->verification_uri_complete == NULL ? "-" + : dc_ctx->verification_uri_complete); + DEBUG(SSSDBG_TRACE_ALL, "message: [%s].\n", dc_ctx->message); + } + DEBUG(SSSDBG_TRACE_ALL, "device_code: [%s].\n", dc_ctx->device_code); + DEBUG(SSSDBG_TRACE_ALL, "expires_in: [%d].\n", dc_ctx->expires_in); + DEBUG(SSSDBG_TRACE_ALL, "interval: [%d].\n", dc_ctx->interval); +} + +void trace_tokens(struct devicecode_ctx *dc_ctx) +{ + char *tmp; + if (!DEBUG_IS_SET(SSSDBG_TRACE_ALL)) { + return; + } + + if (dc_ctx->td->access_token_payload != NULL) { + tmp = json_dumps(dc_ctx->td->access_token_payload, 0); + DEBUG(SSSDBG_TRACE_ALL, "access_token payload: [%s].\n", tmp); + free(tmp); + + DEBUG(SSSDBG_TRACE_ALL, "User Principal: [%s].\n", json_string_value(json_object_get(dc_ctx->td->access_token_payload, "upn"))); + DEBUG(SSSDBG_TRACE_ALL, "User oid: [%s].\n", json_string_value(json_object_get(dc_ctx->td->access_token_payload, "oid"))); + DEBUG(SSSDBG_TRACE_ALL, "User sub: [%s].\n", json_string_value(json_object_get(dc_ctx->td->access_token_payload, "sub"))); + } + + if (dc_ctx->td->id_token_payload != NULL) { + tmp = json_dumps(dc_ctx->td->id_token_payload, 0); + DEBUG(SSSDBG_TRACE_ALL, "id_token payload: [%s].\n", tmp); + free(tmp); + + DEBUG(SSSDBG_TRACE_ALL, "User Principal: [%s].\n", json_string_value(json_object_get(dc_ctx->td->id_token_payload, "upn"))); + DEBUG(SSSDBG_TRACE_ALL, "User oid: [%s].\n", json_string_value(json_object_get(dc_ctx->td->id_token_payload, "oid"))); + DEBUG(SSSDBG_TRACE_ALL, "User sub: [%s].\n", json_string_value(json_object_get(dc_ctx->td->id_token_payload, "sub"))); + } + + tmp = json_dumps(dc_ctx->td->userinfo, 0); + DEBUG(SSSDBG_TRACE_ALL, "userinfo: [%s].\n", tmp); + free(tmp); +} + +int main(int argc, const char *argv[]) +{ + struct cli_opts opts = { 0 }; + errno_t ret; + json_error_t json_error; + TALLOC_CTX *main_ctx = NULL; + struct devicecode_ctx *dc_ctx; + const char *user_identifier = NULL; + int exit_status = EXIT_FAILURE; + + ret = parse_cli(argc, argv, &opts); + if (ret != EOK) { + goto done; + } + + DEBUG_INIT(debug_level, opts.opt_logger); + + DEBUG(SSSDBG_TRACE_FUNC, "oidc_child started.\n"); + + DEBUG(SSSDBG_TRACE_INTERNAL, + "Running with effective IDs: [%"SPRIuid"][%"SPRIgid"].\n", + geteuid(), getegid()); + + DEBUG(SSSDBG_TRACE_INTERNAL, + "Running with real IDs [%"SPRIuid"][%"SPRIgid"].\n", + getuid(), getgid()); + + main_ctx = talloc_new(NULL); + if (main_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed.\n"); + talloc_free(discard_const(debug_prg_name)); + goto done; + } + talloc_steal(main_ctx, debug_prg_name); + + dc_ctx = get_dc_ctx(main_ctx, opts.libcurl_debug, opts.ca_db, + opts.issuer_url, + opts.device_auth_endpoint, opts.token_endpoint, + opts.userinfo_endpoint, opts.jwks_uri, opts.scope); + if (dc_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize main context.\n"); + goto done; + } + + if (opts.get_device_code) { + if (opts.client_secret_stdin) { + ret = read_client_secret_from_stdin(dc_ctx, &opts.client_secret); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to read client secret from stdin.\n"); + goto done; + } + } + + ret = get_devicecode(dc_ctx, opts.client_id, opts.client_secret); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get device code.\n"); + goto done; + } + } + + if (opts.get_access_token) { + if (dc_ctx->device_code == NULL) { + ret = read_device_code_from_stdin(dc_ctx, + opts.client_secret_stdin + ? &opts.client_secret + : NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to read device code from stdin.\n"); + goto done; + } + } + } + + ret = parse_result(dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to parse device code reply.\n"); + goto done; + } + + trace_device_code(dc_ctx, opts.get_device_code); + + ret = get_token(main_ctx, dc_ctx, opts.client_id, opts.client_secret, + opts.get_device_code); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get user token.\n"); + goto done; + } + + if (opts.get_device_code) { + /* Currently this reply is used by ipa-otpd as RADIUS Proxy-State and + * Reply-Message. + */ + fprintf(stdout, + "{\"device_code\":\"%s\",\"expires_in\":%d,\"interval\":%d}\n", + dc_ctx->device_code, dc_ctx->expires_in, dc_ctx->interval); + fprintf(stdout, + "oauth2 {\"verification_uri\": \"%s\", " + "\"user_code\": \"%s%s%s\"}\n", + dc_ctx->verification_uri, dc_ctx->user_code, + dc_ctx->verification_uri_complete == NULL ? "" + : "\", \"verification_uri_complete\": \"", + dc_ctx->verification_uri_complete == NULL ? "" + : dc_ctx->verification_uri_complete); + fflush(stdout); + } + + if (opts.get_access_token) { + DEBUG(SSSDBG_TRACE_ALL, "access_token: [%s].\n", + dc_ctx->td->access_token_str); + DEBUG(SSSDBG_TRACE_ALL, "id_token: [%s].\n", dc_ctx->td->id_token_str); + + if (dc_ctx->jwks_uri != NULL) { + ret = verify_token(dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to verify tokens.\n"); + goto done; + } + } + + ret = get_userinfo(dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get userinfo.\n"); + goto done; + } + + dc_ctx->td->userinfo = json_loads(dc_ctx->http_data, 0, &json_error); + if (dc_ctx->td->userinfo == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to parse userinfo data on line [%d]: [%s].\n", + json_error.line, json_error.text); + goto done; + } + + trace_tokens(dc_ctx); + + user_identifier = get_user_identifier(dc_ctx, dc_ctx->td->userinfo, + opts.user_identifier_attr); + if (user_identifier == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get user identifier.\n"); + goto done; + } + + DEBUG(SSSDBG_CONF_SETTINGS, "User identifier: [%s].\n", + user_identifier); + + fprintf(stdout,"%s", user_identifier); + fflush(stdout); + } + + DEBUG(SSSDBG_IMPORTANT_INFO, "oidc_child finished successful!\n"); + exit_status = EXIT_SUCCESS; + +done: + if (exit_status != EXIT_SUCCESS) { + DEBUG(SSSDBG_IMPORTANT_INFO, "oidc_child failed!\n"); + } + close(STDOUT_FILENO); + talloc_free(main_ctx); + return exit_status; +} diff --git a/src/oidc_child/oidc_child_curl.c b/src/oidc_child/oidc_child_curl.c new file mode 100644 index 0000000..cf09760 --- /dev/null +++ b/src/oidc_child/oidc_child_curl.c @@ -0,0 +1,524 @@ +/* + SSSD + + Helper child for OIDC and OAuth 2.0 Device Authorization Grant + Curl based HTTP access + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2022 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <curl/curl.h> +#include "oidc_child/oidc_child_util.h" + +char *url_encode_string(TALLOC_CTX *mem_ctx, const char *inp) +{ + CURL *curl_ctx = NULL; + char *tmp; + char *out = NULL; + + if (inp == NULL) { + DEBUG(SSSDBG_TRACE_ALL, "Empty input.\n"); + return NULL; + } + + curl_ctx = curl_easy_init(); + if (curl_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize curl.\n"); + return NULL; + } + + tmp = curl_easy_escape(curl_ctx, inp, 0); + if (tmp == NULL) { + DEBUG(SSSDBG_TRACE_ALL, "curl_easy_escape failed for [%s].\n", inp); + goto done; + } + + out = talloc_strdup(mem_ctx, tmp); + curl_free(tmp); + if (out == NULL) { + DEBUG(SSSDBG_TRACE_ALL, "talloc_strdup failed.\n"); + goto done; + } + +done: + curl_easy_cleanup(curl_ctx); + return (out); +} + +/* The curl write_callback will always append the received data. To start a + * new string call clean_http_data() before the curl request.*/ +void clean_http_data(struct devicecode_ctx *dc_ctx) +{ + talloc_free(dc_ctx->http_data); + dc_ctx->http_data = NULL; +} + +static size_t write_callback(char *ptr, size_t size, size_t nmemb, + void *userdata) +{ + size_t realsize = size * nmemb; + struct devicecode_ctx *dc_ctx = (struct devicecode_ctx *) userdata; + char *tmp = NULL; + + DEBUG(SSSDBG_TRACE_ALL, "%*s\n", (int) realsize, ptr); + + tmp = talloc_asprintf(dc_ctx, "%s%*s", + dc_ctx->http_data == NULL ? "" : dc_ctx->http_data, + (int) realsize, ptr); + talloc_free(dc_ctx->http_data); + explicit_bzero(ptr, realsize); + dc_ctx->http_data = tmp; + if (dc_ctx->http_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to copy received data.\n"); + return 0; + } + talloc_set_destructor((void *) dc_ctx->http_data, + sss_erase_talloc_mem_securely); + + return realsize; +} + +static int libcurl_debug_callback(CURL *curl_ctx, curl_infotype type, + char *data, size_t size, void *userptr) +{ + static const char prefix[CURLINFO_END][3] = { + "* ", "< ", "> ", "{ ", "} ", "{ ", "} " }; + + switch (type) { + case CURLINFO_TEXT: + case CURLINFO_HEADER_IN: + case CURLINFO_HEADER_OUT: + sss_debug_fn(__FILE__, __LINE__, __FUNCTION__, SSSDBG_TRACE_ALL, + "libcurl: %s%.*s", prefix[type], (int) size, data); + break; + default: + break; + } + + return 0; +} + +static errno_t set_http_opts(CURL *curl_ctx, struct devicecode_ctx *dc_ctx, + const char *uri, const char *post_data, + const char *token, struct curl_slist *headers) +{ + CURLcode res; + int ret; + + /* Only allow https */ + res = curl_easy_setopt(curl_ctx, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to enforce HTTPS.\n"); + ret = EIO; + goto done; + } + + if (dc_ctx->ca_db != NULL) { + res = curl_easy_setopt(curl_ctx, CURLOPT_CAINFO, dc_ctx->ca_db); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set CA DB path.\n"); + ret = EIO; + goto done; + } + } + + res = curl_easy_setopt(curl_ctx, CURLOPT_URL, uri); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set URL.\n"); + ret = EIO; + goto done; + } + + if (dc_ctx->libcurl_debug) { + res = curl_easy_setopt(curl_ctx, CURLOPT_VERBOSE, 1L); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set verbose option.\n"); + ret = EIO; + goto done; + } + res = curl_easy_setopt(curl_ctx, CURLOPT_DEBUGFUNCTION, + libcurl_debug_callback); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set debug callback.\n"); + ret = EIO; + goto done; + } + } + + res = curl_easy_setopt(curl_ctx, CURLOPT_USERAGENT, "SSSD oidc_child/0.0"); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set useragent option.\n"); + ret = EIO; + goto done; + } + + if (headers != NULL) { + res = curl_easy_setopt(curl_ctx, CURLOPT_HTTPHEADER, headers); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add header to POST request.\n"); + ret = EIO; + goto done; + } + } + + res = curl_easy_setopt(curl_ctx, CURLOPT_WRITEFUNCTION, write_callback); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add write callback.\n"); + ret = EIO; + goto done; + } + + res = curl_easy_setopt(curl_ctx, CURLOPT_WRITEDATA, dc_ctx); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add write callback data.\n"); + ret = EIO; + goto done; + } + + if (post_data != NULL) { + DEBUG(SSSDBG_TRACE_ALL, "POST data: [%s].\n", post_data); + res = curl_easy_setopt(curl_ctx, CURLOPT_POSTFIELDS, post_data); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add data to POST request.\n"); + ret = EIO; + goto done; + } + } + + if (token != NULL) { + res = curl_easy_setopt(curl_ctx, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set HTTP auth.\n"); + ret = EIO; + goto done; + } + res = curl_easy_setopt(curl_ctx, CURLOPT_XOAUTH2_BEARER, token); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add token.\n"); + ret = EIO; + goto done; + } + } + + ret = EOK; +done: + + return ret; +} + +#define ACCEPT_JSON "Accept: application/json" + +static errno_t do_http_request(struct devicecode_ctx *dc_ctx, const char *uri, + const char *post_data, const char *token) +{ + CURL *curl_ctx = NULL; + CURLcode res; + int ret; + long resp_code; + struct curl_slist *headers = NULL; + + headers = curl_slist_append(headers, ACCEPT_JSON); + if (headers == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to create Accept header, trying without.\n"); + } + + curl_ctx = curl_easy_init(); + if (curl_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize curl.\n"); + ret = EIO; + goto done; + } + + ret = set_http_opts(curl_ctx, dc_ctx, uri, post_data, token, headers); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set http options.\n"); + goto done; + } + + res = curl_easy_perform(curl_ctx); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to send request.\n"); + ret = EIO; + goto done; + } + + res = curl_easy_getinfo(curl_ctx, CURLINFO_RESPONSE_CODE, &resp_code); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get response code.\n"); + ret = EIO; + goto done; + } + + if (resp_code != 200) { + DEBUG(SSSDBG_OP_FAILURE, "Request failed, response code is [%ld].\n", + resp_code); + ret = EIO; + goto done; + } + + ret = EOK; +done: + curl_slist_free_all(headers); + curl_easy_cleanup(curl_ctx); + return ret; +} + +#define AZURE_EXPECT_CODE "The request body must contain the following parameter: 'code'." + +errno_t get_token(TALLOC_CTX *mem_ctx, + struct devicecode_ctx *dc_ctx, const char *client_id, + const char *client_secret, + bool get_device_code) +{ + CURL *curl_ctx = NULL; + CURLcode res; + int ret; + size_t waiting_time = 0; + char *error_description = NULL; + char *post_data = NULL; + const char *post_data_tmpl = "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=%s&%s=%s"; + struct curl_slist *headers = NULL; + bool azure_fallback = false; + + headers = curl_slist_append(headers, ACCEPT_JSON); + if (headers == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to create Accept header, trying without.\n"); + } + + post_data = talloc_asprintf(mem_ctx, post_data_tmpl, client_id, "device_code", + dc_ctx->device_code); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate POST data.\n"); + ret = ENOMEM; + goto done; + } + + if (client_secret != NULL) { + post_data = talloc_asprintf_append(post_data, "&client_secret=%s", + client_secret); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client secret to POST data.\n"); + ret = ENOMEM; + goto done; + } + } + + curl_ctx = curl_easy_init(); + if (curl_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize curl.\n"); + ret = EIO; + goto done; + } + + ret = set_http_opts(curl_ctx, dc_ctx, dc_ctx->token_endpoint, post_data, + NULL, headers); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set http options.\n"); + goto done; + } + + do { + clean_http_data(dc_ctx); + + res = curl_easy_perform(curl_ctx); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to send token request.\n"); + ret = EIO; + goto done; + } + + talloc_zfree(error_description); + ret = parse_token_result(dc_ctx, &error_description); + if (ret != EAGAIN) { + if (ret == EIO && !azure_fallback && error_description != NULL + && strstr(error_description, AZURE_EXPECT_CODE) != NULL) { + /* Older Azure AD v1 endpoints expect 'code' instead of the RFC + * conforming 'device_code', see e.g. + * https://docs.microsoft.com/de-de/archive/blogs/azuredev/assisted-login-using-the-oauth-deviceprofile-flow + * and search for 'request_content' in the code example. */ + talloc_free(post_data); + post_data = talloc_asprintf(mem_ctx, post_data_tmpl, client_id, "code", + dc_ctx->device_code); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate POST data.\n"); + ret = ENOMEM; + goto done; + } + azure_fallback = true; + continue; + } + break; + } + + /* only run once after getting the device code to tell the IdP we are + * expecting that the user will connect */ + if (get_device_code) { + if (ret == EAGAIN) { + ret = EOK; + } + break; + } + + waiting_time += dc_ctx->interval; + if (waiting_time >= dc_ctx->expires_in) { + /* Next sleep will end after the request is expired on the + * server side, so we can just error out now. */ + ret = ETIMEDOUT; + break; + } + sleep(dc_ctx->interval); + } while (waiting_time < dc_ctx->expires_in); + + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get token.\n"); + } + +done: + talloc_free(post_data); + talloc_free(error_description); + curl_slist_free_all(headers); + curl_easy_cleanup(curl_ctx); + return ret; +} + +errno_t get_openid_configuration(struct devicecode_ctx *dc_ctx, + const char *issuer_url) +{ + int ret; + char *uri = NULL; + bool has_slash = false; + + if (issuer_url[strlen(issuer_url) - 1] == '/') { + has_slash = true; + } + + uri = talloc_asprintf(dc_ctx, "%s%s.well-known/openid-configuration", + issuer_url, has_slash ? "" : "/"); + if (uri == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to allocate memory for config url.\n"); + ret = ENOMEM; + goto done; + } + + clean_http_data(dc_ctx); + ret = do_http_request(dc_ctx, uri, NULL, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "http request failed.\n"); + } + +done: + talloc_free(uri); + + return ret; +} + +#define DEFAULT_SCOPE "user" + +errno_t get_devicecode(struct devicecode_ctx *dc_ctx, + const char *client_id, const char *client_secret) +{ + int ret; + + char *post_data = NULL; + + post_data = talloc_asprintf(dc_ctx, "client_id=%s&scope=%s", + client_id, + dc_ctx->scope != NULL ? dc_ctx->scope + : DEFAULT_SCOPE); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to allocate memory for POST data.\n"); + return ENOMEM; + } + + if (client_secret != NULL) { + post_data = talloc_asprintf_append(post_data, "&client_secret=%s", + client_secret); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client secret to POST data.\n"); + return ENOMEM; + } + } + + clean_http_data(dc_ctx); + ret = do_http_request(dc_ctx, dc_ctx->device_authorization_endpoint, + post_data, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to send device code request.\n"); + } + + talloc_free(post_data); + return ret; +} + +errno_t get_userinfo(struct devicecode_ctx *dc_ctx) +{ + int ret; + + clean_http_data(dc_ctx); + ret = do_http_request(dc_ctx, dc_ctx->userinfo_endpoint, NULL, + dc_ctx->td->access_token_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to send userinfo request.\n"); + } + + return ret; +} + +errno_t get_jwks(struct devicecode_ctx *dc_ctx) +{ + int ret; + + clean_http_data(dc_ctx); + ret = do_http_request(dc_ctx, dc_ctx->jwks_uri, NULL, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to read jwks file [%s].\n", + dc_ctx->jwks_uri); + } + + return ret; + +} + +static int cleanup_curl(void *p) +{ + curl_global_cleanup(); + + return 0; +} + +errno_t init_curl(void *p) +{ + CURLcode res; + + res = curl_global_init(CURL_GLOBAL_ALL); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize libcurl.\n"); + return EIO; + } + + if (p != NULL) { + talloc_set_destructor(p, cleanup_curl); + } + + return EOK; +} diff --git a/src/oidc_child/oidc_child_json.c b/src/oidc_child/oidc_child_json.c new file mode 100644 index 0000000..a89794c --- /dev/null +++ b/src/oidc_child/oidc_child_json.c @@ -0,0 +1,511 @@ + +/* + SSSD + + Helper child for OIDC and OAuth 2.0 Device Authorization Grant + JSON utilities + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2022 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <jose/jws.h> +#include <jose/b64.h> +#include <jansson.h> + +#include "util/strtonum.h" +#include "oidc_child/oidc_child_util.h" + +static char *get_json_string(TALLOC_CTX *mem_ctx, const json_t *root, + const char *attr) +{ + json_t *tmp; + char *str; + + tmp = json_object_get(root, attr); + if (!json_is_string(tmp)) { + DEBUG(SSSDBG_OP_FAILURE, + "Result does not contain the '%s' string.\n", attr); + return NULL; + } + + str = talloc_strdup(mem_ctx, json_string_value(tmp)); + if (str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to copy '%s' string.\n", attr); + return NULL; + } + + return str; +} + +static int get_json_integer(const json_t *root, const char *attr, + bool fallback_to_string) +{ + json_t *tmp; + int val; + char *endptr; + + tmp = json_object_get(root, attr); + if (!json_is_integer(tmp)) { + if (fallback_to_string) { + if (!json_is_string(tmp) || json_string_value(tmp)== NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Result does not contain the '%s' integer or string.\n", + attr); + return -1; + } + + val = (int) strtoint32(json_string_value(tmp), &endptr, 10); + if (errno != 0 || *endptr != '\0') { + DEBUG(SSSDBG_OP_FAILURE, + "Value [%s] of attribute [%s] is not a valid integer.\n", + json_string_value(tmp), attr); + return -1; + } + return val; + } else { + DEBUG(SSSDBG_OP_FAILURE, + "Result does not contain the '%s' integer.\n", attr); + return -1; + } + } + + return json_integer_value(tmp); +} + +static char *get_json_scope(TALLOC_CTX *mem_ctx, const json_t *root, + const char *attr) +{ + json_t *tmp; + json_t *s; + size_t index; + char *str = NULL; + + tmp = json_object_get(root, attr); + if (!json_is_array(tmp)) { + DEBUG(SSSDBG_OP_FAILURE, + "Result does not contain the '%s' array.\n", attr); + return NULL; + } + + json_array_foreach(tmp, index, s) { + if (!json_is_string(s)) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to read supported scopes.\n"); + talloc_free(str); + return NULL; + } + + if (str == NULL) { + str = talloc_strdup(mem_ctx, json_string_value(s)); + } else { + str = talloc_asprintf_append(str, "%%20%s", json_string_value(s)); + } + if (str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to copy '%s' string.\n", attr); + return NULL; + } + + } + + return str; +} + +static errno_t get_endpoints(json_t *inp, struct devicecode_ctx *dc_ctx) +{ + int ret; + + dc_ctx->device_authorization_endpoint = get_json_string(dc_ctx, inp, + "device_authorization_endpoint"); + if (dc_ctx->device_authorization_endpoint == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing device_authorization_endpoint in " + "openid configuration.\n"); + ret = EINVAL; + goto done; + } + dc_ctx->token_endpoint = get_json_string(dc_ctx, inp, "token_endpoint"); + if (dc_ctx->token_endpoint == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing token_endpoint in openid " + "configuration.\n"); + ret = EINVAL; + goto done; + } + dc_ctx->userinfo_endpoint = get_json_string(dc_ctx, inp, + "userinfo_endpoint"); + if (dc_ctx->userinfo_endpoint == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing userinfo_endpoint in openid " + "configuration.\n"); + ret = EINVAL; + goto done; + } + + dc_ctx->jwks_uri = get_json_string(dc_ctx, inp, "jwks_uri"); + if (dc_ctx->jwks_uri == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing jwks_uri in openid " + "configuration.\n"); + } + + dc_ctx->scope = get_json_scope(dc_ctx, inp, "scopes_supported"); + if (dc_ctx->scope == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing scopes in openid " + "configuration.\n"); + } + + ret = EOK; +done: + return ret; +} + +static errno_t str_to_jws(TALLOC_CTX *mem_ctx, const char *inp, json_t **jws) +{ + char *pl; + char *sig; + json_t *o = NULL; + int ret; + char *str = NULL; + + str = talloc_strdup(mem_ctx, inp); + if (str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to copy token string.\n"); + ret = ENOMEM; + goto done; + } + + pl = strchr(str, '.'); + if (pl == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "String does not look like serialized JWS, missing first '.'\n"); + ret = EINVAL; + goto done; + } + *pl = '\0'; + pl++; + + sig = strchr(pl, '.'); + if (sig == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "String does not look like serialized JWS, missing second '.'\n"); + ret = EINVAL; + goto done; + } + *sig = '\0'; + sig++; + + o = json_object(); + if (o == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to create JSON object.\n"); + ret = EIO; + goto done; + } + + ret = json_object_set_new(o, "protected", json_string(str)); + if (ret == 0) { + ret = json_object_set_new(o, "payload", json_string(pl)); + } + if (ret == 0) { + ret = json_object_set_new(o, "signature", json_string(sig)); + } + if (ret == -1) { + json_decref(o); + DEBUG(SSSDBG_OP_FAILURE, "json_object_set_new() failed.\n"); + ret = EINVAL; + goto done; + } + + *jws = o; + ret = EOK; + +done: + talloc_free(str); + return ret; +} + +/* It looks like not all tokens can be verified even if the keys are read from + * the URL given in the OIDC configuration URL and that it differs between + * different IdPs. For the time being the verification code is called but + * errors in the verification are ignored. But the debug output should help to + * understand if and how the keys based verification can be used so that we + * might add new options to tune the verification for different IdPs. + */ +errno_t verify_token(struct devicecode_ctx *dc_ctx) +{ + int ret; + json_t *keys = NULL; + json_error_t json_error; + json_t *jws = NULL; + + ret = get_jwks(dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to read jwks file.\n"); + goto done; + } + + keys = json_loads(dc_ctx->http_data, 0, &json_error); + if (keys == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to parse jwk data from [%s] on line [%d]: [%s].\n", + dc_ctx->jwks_uri, json_error.line, json_error.text); + ret = EINVAL; + goto done; + } + + if (dc_ctx->td->id_token_str != NULL) { + ret = str_to_jws(dc_ctx, dc_ctx->td->id_token_str, &jws); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to convert id token into jws.\n"); + dc_ctx->td->id_token_payload = NULL; + ret = EOK; + goto done; + } + if (!jose_jws_ver(NULL, jws, NULL, keys, false)) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to verify id_token.\n"); + } + + dc_ctx->td->id_token_payload = jose_b64_dec_load(json_object_get(jws, + "payload")); + + json_decref(jws); + } + if (dc_ctx->td->access_token_str != NULL) { + ret = str_to_jws(dc_ctx, dc_ctx->td->access_token_str, &jws); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to convert access_token into jws.\n"); + dc_ctx->td->access_token_payload = NULL; + ret = EOK; + goto done; + } + if (!jose_jws_ver(NULL, jws, NULL, keys, false)) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to verify access_token.\n"); + } + + dc_ctx->td->access_token_payload = jose_b64_dec_load(json_object_get(jws, + "payload")); + json_decref(jws); + } + + ret = EOK; + +done: + json_decref(keys); + clean_http_data(dc_ctx); + + return ret; +} + +errno_t parse_openid_configuration(struct devicecode_ctx *dc_ctx) +{ + int ret; + json_t *root = NULL; + json_error_t json_error; + + root = json_loads(dc_ctx->http_data, 0, &json_error); + if (root == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to parse json data on line [%d]: [%s].\n", + json_error.line, json_error.text); + ret = EINVAL; + goto done; + } + + ret = get_endpoints(root, dc_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get endpoints.\n"); + goto done; + } + + clean_http_data(dc_ctx); + + ret = EOK; + +done: + json_decref(root); + return ret; +} + +errno_t parse_result(struct devicecode_ctx *dc_ctx) +{ + int ret; + json_t *root = NULL; + json_error_t json_error; + + root = json_loads(dc_ctx->http_data, 0, &json_error); + if (root == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to parse json data on line [%d]: [%s].\n", + json_error.line, json_error.text); + ret = EINVAL; + goto done; + } + + dc_ctx->user_code = get_json_string(dc_ctx, root, "user_code"); + if (dc_ctx->user_code != NULL) { + talloc_set_destructor((void *) dc_ctx->user_code, sss_erase_talloc_mem_securely); + } + dc_ctx->device_code = get_json_string(dc_ctx, root, "device_code"); + if (dc_ctx->device_code != NULL) { + talloc_set_destructor((void *) dc_ctx->device_code, sss_erase_talloc_mem_securely); + } + dc_ctx->verification_uri = get_json_string(dc_ctx, root, + "verification_uri"); + if (dc_ctx->verification_uri == NULL) { + /* Google uses _urL rather than _urI, see e.g. + * https://developers.google.com/identity/protocols/oauth2/limited-input-device + * Old Azure AD v1 endpoints do the same. */ + dc_ctx->verification_uri = get_json_string(dc_ctx, root, + "verification_url"); + } + dc_ctx->verification_uri_complete = get_json_string(dc_ctx, root, + "verification_uri_complete"); + dc_ctx->message = get_json_string(dc_ctx, root, "message"); + dc_ctx->interval = get_json_integer(root, "interval", true); + dc_ctx->expires_in = get_json_integer(root, "expires_in", true); + + ret = EOK; + +done: + json_decref(root); + return ret; +} + +static int token_destructor(void *p) +{ + struct token_data *td = talloc_get_type(p, struct token_data); + + json_decref(td->result); + + return 0; +} + +errno_t parse_token_result(struct devicecode_ctx *dc_ctx, + char **error_description) +{ + json_t *tmp = NULL; + json_error_t json_error; + json_t *result = NULL; + + *error_description = NULL; + result = json_loads(dc_ctx->http_data, 0, &json_error); + if (result == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to parse json data on line [%d]: [%s].\n", + json_error.line, json_error.text); + return EINVAL; + } + + tmp = json_object_get(result, "error"); + if (json_is_string(tmp)) { + if (strcmp(json_string_value(tmp), "authorization_pending") == 0) { + json_decref(result); + return EAGAIN; + } else if (strcmp(json_string_value(tmp), "slow_down") == 0) { + /* RFC 8628: "... the interval MUST be increased by 5 seconds for" + * "this and all subsequent requests." */ + dc_ctx->interval += 5; + json_decref(result); + return EAGAIN; + } else { + *error_description = get_json_string(dc_ctx, result, + "error_description"); + DEBUG(SSSDBG_OP_FAILURE, "Token request failed with [%s][%s].\n", + json_string_value(tmp), + *error_description); + json_decref(result); + return EIO; + } + } + + /* Looks like we got the tokens */ + dc_ctx->td = talloc_zero(dc_ctx, struct token_data); + if (dc_ctx->td == NULL) { + json_decref(result); + DEBUG(SSSDBG_OP_FAILURE, + "Failed to allocate memory for token data.\n"); + return ENOMEM; + } + talloc_set_destructor((void *) dc_ctx->td, token_destructor); + dc_ctx->td->result = result; + dc_ctx->td->access_token = json_object_get(dc_ctx->td->result, + "access_token"); + dc_ctx->td->access_token_str = get_json_string(dc_ctx->td, + dc_ctx->td->result, + "access_token"); + dc_ctx->td->id_token = json_object_get(dc_ctx->td->result, "id_token"); + dc_ctx->td->id_token_str = get_json_string(dc_ctx->td, dc_ctx->td->result, + "id_token"); + + return EOK; +} + +static const char *get_id_string(TALLOC_CTX *mem_ctx, json_t *id_object) +{ + switch (json_typeof(id_object)) { + case JSON_STRING: + return talloc_strdup(mem_ctx, json_string_value(id_object)); + break; + case JSON_INTEGER: + return talloc_asprintf(mem_ctx, "%" JSON_INTEGER_FORMAT, + json_integer_value(id_object)); + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "Unexpected user identifier type.\n"); + } + + return NULL; +} + +const char *get_user_identifier(TALLOC_CTX *mem_ctx, json_t *userinfo, + const char *user_identifier_attr) +{ + json_t *id_object = NULL; + const char *user_identifier = NULL; + const char *id_attr_list[] = { "sub", "id", NULL }; + size_t c; + + if (user_identifier_attr != NULL) { + id_attr_list[0] = user_identifier_attr; + id_attr_list[1] = NULL; + } + + for (c = 0; id_attr_list[c] != NULL; c++) { + id_object = json_object_get(userinfo, id_attr_list[c]); + if (id_object != NULL) { + user_identifier = get_id_string(mem_ctx, id_object); + if (user_identifier == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to get user identifier string.\n"); + } + break; + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to read attribute [%s] from userinfo data.\n", + id_attr_list[c]); + } + } + + if (user_identifier == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "No attribute to identify the user found.\n"); + } else { + DEBUG(SSSDBG_CONF_SETTINGS, "User identifier: [%s].\n", + user_identifier); + } + + return user_identifier; +} diff --git a/src/oidc_child/oidc_child_util.h b/src/oidc_child/oidc_child_util.h new file mode 100644 index 0000000..8b106ae --- /dev/null +++ b/src/oidc_child/oidc_child_util.h @@ -0,0 +1,99 @@ +/* + SSSD + + Helper child for OIDC and OAuth 2.0 Device Authorization Grant + + Authors: + Sumit Bose <sbose@redhat.com> + + Copyright (C) 2022 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef __OIDC_CHILD_UTIL_H__ +#define __OIDC_CHILD_UTIL_H__ + +#include <jansson.h> +#include "util/util.h" + +struct token_data { + json_t *result; + json_t *access_token; + json_t *access_token_payload; + char *access_token_str; + json_t *id_token; + json_t *id_token_payload; + char *id_token_str; + json_t *userinfo; +}; + +struct devicecode_ctx { + bool libcurl_debug; + const char *ca_db; + const char *device_authorization_endpoint; + const char *token_endpoint; + const char *userinfo_endpoint; + const char *jwks_uri; + const char *scope; + + char *http_data; + char *user_code; + char *device_code; + char *verification_uri; + char *verification_uri_complete; + char *message; + int interval; + int expires_in; + + struct token_data *td; +}; + +/* oidc_child_curl.c */ +char *url_encode_string(TALLOC_CTX *mem_ctx, const char *inp); + +errno_t init_curl(void *p); + +void clean_http_data(struct devicecode_ctx *dc_ctx); + +errno_t get_openid_configuration(struct devicecode_ctx *dc_ctx, + const char *issuer_url); + +errno_t get_jwks(struct devicecode_ctx *dc_ctx); + +errno_t get_devicecode(struct devicecode_ctx *dc_ctx, + const char *client_id, const char *client_secret); + +errno_t get_token(TALLOC_CTX *mem_ctx, + struct devicecode_ctx *dc_ctx, const char *client_id, + const char *client_secret, + bool get_device_code); + +errno_t get_userinfo(struct devicecode_ctx *dc_ctx); + + +/* oidc_child_json.c */ +errno_t parse_openid_configuration(struct devicecode_ctx *dc_ctx); + +errno_t parse_result(struct devicecode_ctx *dc_ctx); + +errno_t parse_token_result(struct devicecode_ctx *dc_ctx, + char **error_description); + +errno_t verify_token(struct devicecode_ctx *dc_ctx); + +const char *get_user_identifier(TALLOC_CTX *mem_ctx, json_t *userinfo, + const char *user_identifier_attr); + +#endif /* __OIDC_CHILD_UTIL_H__ */ |