summaryrefslogtreecommitdiffstats
path: root/src/oidc_child
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/oidc_child/oidc_child.c636
-rw-r--r--src/oidc_child/oidc_child_curl.c524
-rw-r--r--src/oidc_child/oidc_child_json.c511
-rw-r--r--src/oidc_child/oidc_child_util.h99
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__ */