/* * FreeIPA 2FA companion daemon * * Authors: Sumit Bose * * Copyright (C) 2021 Red Hat * see file 'COPYING' for use and warranty information * * 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 . */ /* * This file reaches out to a third-party IdP to handle an OAuth2 * authentication request (stdio.c/query.c) if the user is configured * accordingly. The result is placed in the stdout queue (stdio.c). */ #include #include #include #include #include #include "internal.h" #define OIDC_CHILD_PATH "/usr/libexec/sssd/oidc_child" struct child_ctx { int read_from_child; int write_to_child; verto_ev *read_ev; verto_ev *write_ev; verto_ev *child_ev; struct otpd_queue_item *item; struct otpd_queue_item *saved_item; enum oauth2_state oauth2_state; }; static int set_fd_nonblocking(int fd) { int flags; int ret; flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { ret = errno; return ret; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { ret = errno; return ret; } return 0; } static void free_child_ctx(verto_ctx *vctx, verto_ev *ev) { (void)vctx; /* Unused */ struct child_ctx *child_ctx; child_ctx = verto_get_private(ev); free(child_ctx); } static void oauth2_on_child_exit(verto_ctx *vctx, verto_ev *ev) { (void)vctx; /* Unused */ verto_proc_status st; st = verto_get_proc_status(ev); /* The krad req might not be available at this stage anymore, so * otpd_log_err() is used. */ otpd_log_err(0, "Child finished with status [%d].", WEXITSTATUS(st)); } static void oauth2_on_child_writable(verto_ctx *vctx, verto_ev *ev) { (void)vctx; /* Unused */ ssize_t io; struct child_ctx *child_ctx; child_ctx = verto_get_private(ev); if (child_ctx == NULL) { otpd_log_err(EINVAL, "Lost child context"); verto_del(ev); return; } if (child_ctx->oauth2_state == OAUTH2_GET_DEVICE_CODE) { /* no input needed */ verto_del(ev); return; } io = write(verto_get_fd(ev), child_ctx->saved_item->oauth2.device_code_reply, strlen(child_ctx->saved_item->oauth2.device_code_reply)); otpd_queue_item_free(child_ctx->saved_item); if (io < 0) { switch (errno) { #if defined(EWOULDBLOCK) && (!defined(EAGAIN) || EAGAIN - EWOULDBLOCK != 0) case EWOULDBLOCK: #endif #if defined(EAGAIN) case EAGAIN: #endif case ENOBUFS: case EINTR: /* In this case, we just need to try again. */ return; default: /* Unrecoverable. */ break; } otpd_log_err(errno, "Failed to send to child"); } verto_del(ev); } #define min(a,b) ((a) > (b) ? (b) : (a)) static int add_krad_attr_to_set(struct child_ctx *child_ctx, krad_attrset *attrset, krb5_data *datap, krad_attr attr, const char *message) { krb5_data state = {0}; char *p = datap->data; unsigned int len = datap->length; int ret = 0; do { state.data = p; state.length = min(MAX_ATTRSIZE - 5, len); p += state.length; ret = krad_attrset_add(attrset, attr, &(state)); if (ret != 0) { otpd_log_req(child_ctx->item->req, message); break; } len -= state.length; } while (len > 0); return ret; } /* Most attributes have limited length (MAX_ATTRSIZE). In order to accept longer * values, we will concatenate all the attribute values to single krb5_data. */ static int get_krad_attr_from_packet(const krad_packet *rres, krad_attr attr, krb5_data *_data) { const krb5_data *rmsg; krb5_data data = {0}; unsigned int memindex; unsigned int i; i = 0; do { rmsg = krad_packet_get_attr(rres, attr, i); if (rmsg != NULL) { data.length += rmsg->length; } i++; } while (rmsg != NULL); if (data.length == 0) { return ENOENT; } data.data = malloc(data.length); if (data.data == NULL) { return ENOMEM; } i = 0; memindex = 0; do { rmsg = krad_packet_get_attr(rres, attr, i); if (rmsg != NULL) { memcpy(&data.data[memindex], rmsg->data, rmsg->length); memindex += rmsg->length; } i++; } while (rmsg != NULL); if (memindex != data.length) { free(data.data); return ERANGE; } *_data = data; return 0; } /* oidc_child will return two lines. * The first is a JSON formatted string containing the device code and other * data needed to get the access token in the second round. This will be * returned to the caller as Radius Proxy-State so that the caller will send * it back in the next round. * The second line is the string expected by the krb5 oauth2 pre-auth plugin * and will be send to the caller as Radius Reply-Message. */ static int handle_device_code_reply(struct child_ctx *child_ctx, const char *dc_reply, char *rad_reply) { krad_attrset *attrset = NULL; int ret; krb5_data data = { 0 }; struct otpd_queue_item *state_item; ret = otpd_queue_item_new(NULL, &state_item); if (ret != 0) { otpd_log_req(child_ctx->item->req, "Failed to allocate state item"); goto done; } state_item->oauth2.device_code_reply = strdup(dc_reply); if (state_item->oauth2.device_code_reply == NULL) { otpd_log_req(child_ctx->item->req, "Failed to copy device code reply."); goto done; } ret = krad_attrset_new(ctx.kctx, &attrset); if (ret != 0) { otpd_log_req(child_ctx->item->req, "Failed to create radius attribute set"); goto done; } state_item->oauth2.state.magic = 0; state_item->oauth2.state.data = strdup(dc_reply); if (state_item->oauth2.state.data == NULL) { otpd_log_req(child_ctx->item->req, "Failed to copy device code reply to krad."); goto done; } state_item->oauth2.state.length = strlen(dc_reply); ret = add_krad_attr_to_set(child_ctx, attrset, &(state_item->oauth2.state), krad_attr_name2num("Proxy-State"), "Failed to serialize state to attribute set"); if (ret != 0) { goto done; } data.magic = 0; data.data = rad_reply; data.length = strlen(rad_reply); ret = add_krad_attr_to_set(child_ctx, attrset, &data, krad_attr_name2num("Reply-Message"), "Failed to serialize reply to attribute set"); if (ret != 0) { goto done; } ret = krad_packet_new_response(ctx.kctx, SECRET, krad_code_name2num("Access-Challenge"), attrset, child_ctx->item->req, &child_ctx->item->rsp); if (ret != 0) { otpd_log_err(ret, "Failed to create radius response"); child_ctx->item->rsp = NULL; } otpd_queue_push(&ctx.oauth2_state.states, state_item); ret = 0; done: krad_attrset_free(attrset); if (ret != 0) { if (state_item != NULL) { free(state_item->oauth2.state.data); free(state_item->oauth2.device_code_reply); free(state_item); } } return ret; } static int check_access_token_reply(struct child_ctx *child_ctx, const char *buf, size_t len) { int ret; if (strlen(child_ctx->item->user.ipaidpSub) != len || memcmp(child_ctx->item->user.ipaidpSub, buf, len) != 0) { return EPERM; } ret = krad_packet_new_response(ctx.kctx, SECRET, krad_code_name2num("Access-Accept"), NULL, child_ctx->item->req, &child_ctx->item->rsp); if (ret != 0) { otpd_log_err(ret, "Failed to create radius response"); child_ctx->item->rsp = NULL; } return ret; } static void oauth2_on_child_readable(verto_ctx *, verto_ev *ev) { static char buf[10240]; ssize_t io = 0; struct child_ctx *child_ctx = NULL; int ret; char *rad_reply; char *end; child_ctx = (struct child_ctx *) verto_get_private(ev); if (child_ctx == NULL) { otpd_log_err(EINVAL, "Lost child context"); verto_del(ev); return; } /* Make sure ctx.stdio.responses will at least return an error */ child_ctx->item->rsp = NULL; child_ctx->item->sent = 0; io = read(verto_get_fd(ev), buf, 10240); if (io < 0) { otpd_log_err(errno, "Failed to read from child"); goto done; } if (io >= 0) { buf[io] = '\0'; otpd_log_req(child_ctx->item->req, "Received: [%s]", buf); } verto_del(ev); if (child_ctx->oauth2_state == OAUTH2_GET_DEVICE_CODE) { /* expect 2 lines of output. First the orginal JSON string return by * the IdP from the devicecode request which will be used as input to * the child process in the second run. Second the JSON string returned * in the radius reply. */ rad_reply = memchr(buf, '\n', io); if (rad_reply != NULL) { *rad_reply = '\0'; rad_reply++; end = memchr(rad_reply, '\n', io - (rad_reply - 1 - buf)); if (end == NULL) { otpd_log_req(child_ctx->item->req, "Missing second new-line."); goto done; } *end = '\0'; ret = handle_device_code_reply(child_ctx, buf, rad_reply); if (ret != 0) { otpd_log_req(child_ctx->item->req, "Failed to handle device code reply."); } } } else if (child_ctx->oauth2_state == OAUTH2_GET_ACCESS_TOKEN) { ret = check_access_token_reply(child_ctx, buf, (size_t) io); if (ret != 0) { otpd_log_req(child_ctx->item->req, "Failed to check access token reply."); } } else { /* error */ otpd_log_req(child_ctx->item->req, "Unexpected state [%d].", child_ctx->oauth2_state); } done: otpd_queue_push(&ctx.stdio.responses, child_ctx->item); verto_set_flags(ctx.stdio.writer, VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_ERROR | VERTO_EV_FLAG_IO_READ | VERTO_EV_FLAG_IO_WRITE); } static const char *oauth2_state_to_str(enum oauth2_state oauth2_state) { switch (oauth2_state) { case OAUTH2_NO: return "OAuth2 not available"; break; case OAUTH2_GET_ISSUER: return "Get issuer from LDAP"; break; case OAUTH2_GET_DEVICE_CODE: return "Get device code"; break; case OAUTH2_GET_ACCESS_TOKEN: return "Get access token"; break; default: return "Unknown OAuth2 state"; } } int oauth2(struct otpd_queue_item **item, enum oauth2_state oauth2_state) { int ret; pid_t child_pid; int pipefd_to_child[2] = { -1, -1}; int pipefd_from_child[2] = { -1, -1}; struct child_ctx *child_ctx; /* Up to 50 arguments to the helper supported. The amount of arguments * is controlled inside this function. Right now max used is below 20 */ char *args[50] = {NULL}; size_t args_idx = 0; krb5_data data_state = {0}; struct otpd_queue_item *saved_item; if (oauth2_state != OAUTH2_GET_DEVICE_CODE && oauth2_state != OAUTH2_GET_ACCESS_TOKEN) { otpd_log_req((*item)->req, "Unexpected OAuth2 state [%d][%s]", oauth2_state, oauth2_state_to_str(oauth2_state)); return EINVAL; } if (oauth2_state == OAUTH2_GET_ACCESS_TOKEN) { ret = get_krad_attr_from_packet((*item)->req, krad_attr_name2num("Proxy-State"), &data_state); if ((ret != 0) || (data_state.length == 0)) { otpd_log_req((*item)->req, "Missing Radius Proxy-State attribute"); return EINVAL; } saved_item = calloc(sizeof(struct otpd_queue_item), 1); if (saved_item == NULL) { otpd_log_req((*item)->req, "No matching saved state found"); return EINVAL; } saved_item->oauth2.device_code_reply = strndup(data_state.data, data_state.length); if (saved_item->oauth2.device_code_reply == NULL) { otpd_log_req((*item)->req, "Failed to copy device code reply"); return EINVAL; } krb5_free_data_contents(NULL, &data_state); } child_ctx = calloc(sizeof(struct child_ctx), 1); if (child_ctx == NULL) { ret = ENOMEM; goto done; } child_ctx->item = (*item); child_ctx->saved_item = saved_item; child_ctx->oauth2_state = oauth2_state; otpd_log_req((*item)->req, "oauth2 start: %s", oauth2_state_to_str(oauth2_state)); args[args_idx++] = OIDC_CHILD_PATH; if (oauth2_state == OAUTH2_GET_DEVICE_CODE) { args[args_idx++] = "--get-device-code"; } else { args[args_idx++] = "--get-access-token"; } if ((*item)->idp.ipaidpIssuerURL != NULL) { args[args_idx++] = "--issuer-url"; args[args_idx++] = (*item)->idp.ipaidpIssuerURL; } else { args[args_idx++] = "--device-auth-endpoint"; args[args_idx++] = (*item)->idp.ipaidpDevAuthEndpoint; args[args_idx++] = "--token-endpoint"; args[args_idx++] = (*item)->idp.ipaidpTokenEndpoint; args[args_idx++] = "--userinfo-endpoint"; args[args_idx++] = (*item)->idp.ipaidpUserInfoEndpoint; if ((*item)->idp.ipaidpKeysEndpoint) { args[args_idx++] = "--jwks-uri"; args[args_idx++] = (*item)->idp.ipaidpKeysEndpoint; } } args[args_idx++] = "--client-id"; args[args_idx++] = (*item)->idp.ipaidpClientID; if ((*item)->idp.ipaidpClientSecret) { args[args_idx++] = "--client-secret"; args[args_idx++] = (*item)->idp.ipaidpClientSecret; } if ((*item)->idp.ipaidpScope) { args[args_idx++] = "--scope"; args[args_idx++] = (*item)->idp.ipaidpScope; } if ((*item)->idp.ipaidpSub) { args[args_idx++] = "--user-identifier-attribute"; args[args_idx++] = (*item)->idp.ipaidpSub; } if ((*item)->idp.ipaidpDebugLevelStr != NULL) { args[args_idx++] = "--debug-level"; args[args_idx++] = (*item)->idp.ipaidpDebugLevelStr; } if ((*item)->idp.ipaidpDebugCurl) { args[args_idx++] = "--libcurl-debug"; } #if 0 for (int i; args[i]; i++) { otpd_log_req((*item)->req, "oidc_child exec: %s", args[i]); } #endif ret = pipe(pipefd_from_child); if (ret == -1) { ret = errno; goto done; } ret = pipe(pipefd_to_child); if (ret == -1) { ret = errno; goto done; } child_pid = fork(); if (child_pid == 0) { /* child */ close(pipefd_to_child[1]); ret = dup2(pipefd_to_child[0], STDIN_FILENO); if (ret == -1) { exit(EXIT_FAILURE); } close(pipefd_from_child[0]); ret = dup2(pipefd_from_child[1], STDOUT_FILENO); if (ret == -1) { exit(EXIT_FAILURE); } execv(OIDC_CHILD_PATH, args); exit(EXIT_FAILURE); } else if (child_pid > 0) { /* parent */ close(pipefd_to_child[0]); set_fd_nonblocking(pipefd_to_child[1]); child_ctx->write_to_child = pipefd_to_child[1]; close(pipefd_from_child[1]); set_fd_nonblocking(pipefd_from_child[0]); child_ctx->read_from_child = pipefd_from_child[0]; child_ctx->write_ev = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_CLOSE_FD | VERTO_EV_FLAG_IO_ERROR | VERTO_EV_FLAG_IO_WRITE, oauth2_on_child_writable, child_ctx->write_to_child); if (child_ctx->write_ev == NULL) { ret = ENOMEM; otpd_log_err(ret, "Unable to initialize oauth2 writer event"); goto done; } verto_set_private(child_ctx->write_ev, child_ctx, NULL); child_ctx->read_ev = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_CLOSE_FD | VERTO_EV_FLAG_IO_ERROR | VERTO_EV_FLAG_IO_READ, oauth2_on_child_readable, child_ctx->read_from_child); if (child_ctx->read_ev == NULL) { ret = ENOMEM; otpd_log_err(ret, "Unable to initialize oauth2 writer event"); goto done; } verto_set_private(child_ctx->read_ev, child_ctx, NULL); child_ctx->child_ev = verto_add_child(ctx.vctx, VERTO_EV_FLAG_NONE, oauth2_on_child_exit, child_pid); verto_set_private(child_ctx->child_ev, child_ctx, free_child_ctx); } else { /* error */ ret = errno; otpd_log_err(ret, "Failed to fork oidc_child"); goto done; } ret = 0; done: if (ret == 0) { *item = NULL; } return ret; }