diff --git a/client/man/default.conf.5 b/client/man/default.conf.5 index 4231d2a53..177a11065 100644 --- a/client/man/default.conf.5 +++ b/client/man/default.conf.5 @@ -160,6 +160,9 @@ Specifies the mode the server is running in. The currently support values are \f .B mount_ipa Specifies the mount point that the development server will register. The default is /ipa/ .TP +.B oidc_child_debug_level +Specifies the debug level of \fBoidc_child\fR, a helper process used by \fBipa-otpd\fR for OIDC/OAuth2 authentication. Level can be between 0 and 10, the higher the more details. If the level is 6 or higher HTTP debug output is added as well. +.TP .B prompt_all Specifies that all options should be prompted for in the IPA client, even optional values. Default is False. .TP diff --git a/daemons/ipa-otpd/Makefile.am b/daemons/ipa-otpd/Makefile.am index 975a0c935..bf856cce1 100644 --- a/daemons/ipa-otpd/Makefile.am +++ b/daemons/ipa-otpd/Makefile.am @@ -9,7 +9,8 @@ ipa_otpd_LDADD = $(top_builddir)/util/libutil.la dist_noinst_DATA = ipa-otpd.socket.in ipa-otpd@.service.in test.py systemdsystemunit_DATA = ipa-otpd.socket ipa-otpd@.service -ipa_otpd_SOURCES = bind.c forward.c main.c parse.c query.c queue.c stdio.c +ipa_otpd_SOURCES = bind.c forward.c main.c parse.c query.c queue.c stdio.c \ + oauth2.c %.socket: %.socket.in @sed -e 's|@krb5rundir[@]|$(krb5rundir)|g' \ diff --git a/daemons/ipa-otpd/internal.h b/daemons/ipa-otpd/internal.h index 43096edf0..1d07247df 100644 --- a/daemons/ipa-otpd/internal.h +++ b/daemons/ipa-otpd/internal.h @@ -28,6 +28,13 @@ #include +#ifndef UCHAR_MAX +#define UCHAR_MAX 255 +#endif + +/* RFC 2865 */ +#define MAX_ATTRSIZE (UCHAR_MAX - 2) + #define SECRET "" #define otpd_log_req(req, ...) \ otpd_log_req_(__FILE__, __LINE__, (req), __VA_ARGS__) @@ -36,11 +43,28 @@ struct otpd_queue_iter; +enum ldap_query { + LDAP_QUERY_EMPTY = 0, + LDAP_QUERY_USER, + LDAP_QUERY_RADIUS, + LDAP_QUERY_RADIUS_USERMAP, + LDAP_QUERY_IDP, + LDAP_QUERY_END +}; + +enum oauth2_state { + OAUTH2_NO = 0, + OAUTH2_GET_ISSUER, + OAUTH2_GET_DEVICE_CODE, + OAUTH2_GET_ACCESS_TOKEN +}; + struct otpd_queue_item { struct otpd_queue_item *next; krad_packet *req; krad_packet *rsp; size_t sent; + enum ldap_query ldap_query; char *error; struct { @@ -48,6 +72,9 @@ struct otpd_queue_item { char *uid; char *ipatokenRadiusUserName; char *ipatokenRadiusConfigLink; + char *ipaidpSub; + char *ipaidpConfigLink; + char **ipauserauthtypes; char *other; } user; @@ -58,6 +85,28 @@ struct otpd_queue_item { time_t ipatokenRadiusTimeout; size_t ipatokenRadiusRetries; } radius; + + struct { + char *name; + char *ipaidpIssuerURL; + char *ipaidpDevAuthEndpoint; + char *ipaidpTokenEndpoint; + char *ipaidpUserInfoEndpoint; + char *ipaidpKeysEndpoint; + char *ipaidpClientID; + char *ipaidpClientSecret; + char *ipaidpScope; + char *ipaidpSub; + krb5_boolean valid; + char* ipaidpDebugLevelStr; + krb5_boolean ipaidpDebugCurl; + } idp; + + struct { + char *device_code_reply; + krb5_data state; + } oauth2; + int msgid; }; @@ -98,6 +147,10 @@ struct otpd_context { struct otpd_queue requests; struct otpd_queue responses; } bind; + + struct { + struct otpd_queue states; + } oauth2_state; }; extern struct otpd_context ctx; @@ -143,8 +196,13 @@ krb5_error_code otpd_forward(struct otpd_queue_item **i); const char *otpd_parse_user(LDAP *ldp, LDAPMessage *entry, struct otpd_queue_item *item); +const char *otpd_parse_idp(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item); + const char *otpd_parse_radius(LDAP *ldp, LDAPMessage *entry, struct otpd_queue_item *item); const char *otpd_parse_radius_username(LDAP *ldp, LDAPMessage *entry, struct otpd_queue_item *item); + +int oauth2(struct otpd_queue_item **item, enum oauth2_state); diff --git a/daemons/ipa-otpd/main.c b/daemons/ipa-otpd/main.c index 529e3834a..ab16a05b8 100644 --- a/daemons/ipa-otpd/main.c +++ b/daemons/ipa-otpd/main.c @@ -242,7 +242,8 @@ int main(int argc, char **argv) goto error; } - ctx.vctx = verto_new(NULL, VERTO_EV_TYPE_IO | VERTO_EV_TYPE_SIGNAL); + ctx.vctx = verto_default(NULL, VERTO_EV_TYPE_IO | VERTO_EV_TYPE_SIGNAL + | VERTO_EV_TYPE_CHILD); if (ctx.vctx == NULL) { otpd_log_err(ENOMEM, "Unable to initialize event loop"); goto error; @@ -342,4 +343,3 @@ error: krb5_free_context(ctx.kctx); return ctx.exitstatus; } - diff --git a/daemons/ipa-otpd/oauth2.c b/daemons/ipa-otpd/oauth2.c new file mode 100644 index 000000000..df1ffd50d --- /dev/null +++ b/daemons/ipa-otpd/oauth2.c @@ -0,0 +1,614 @@ +/* + * 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; +} diff --git a/daemons/ipa-otpd/parse.c b/daemons/ipa-otpd/parse.c index 062b64037..897de752e 100644 --- a/daemons/ipa-otpd/parse.c +++ b/daemons/ipa-otpd/parse.c @@ -24,8 +24,11 @@ * This file parses the user's configuration received from LDAP (see query.c). */ +#define _GNU_SOURCE /* for asprintf() */ #include "internal.h" +#include #include +#include #define DEFAULT_TIMEOUT 15 #define DEFAULT_RETRIES 3 @@ -65,6 +68,69 @@ static int get_string(LDAP *ldp, LDAPMessage *entry, const char *name, return 0; } +/* Convert an LDAP entry into an allocated string array. */ +static int get_string_array(LDAP *ldp, LDAPMessage *entry, const char *name, + char ***out) +{ + struct berval **vals; + ber_len_t i; + char **buf; + int tmp; + size_t count; + size_t c; + int ret; + + vals = ldap_get_values_len(ldp, entry, name); + if (vals == NULL) + return ENOENT; + + tmp = ldap_count_values_len(vals); + if (tmp < 0) { + ret = ENOENT; + goto done; + } + count = (size_t) tmp; + + buf = calloc(count + 1, sizeof(char *)); + if (buf == NULL) { + ret = ENOMEM; + goto done; + } + + for (c = 0; c < count; c++) { + buf[c] = calloc(vals[c]->bv_len + 1, sizeof(char)); + if (buf[c] == NULL) { + ret = ENOMEM; + goto done; + } + + for (i = 0; i < vals[c]->bv_len; i++) { + if (!isprint(vals[c]->bv_val[i])) { + ret = EINVAL; + goto done; + } + + buf[c][i] = vals[c]->bv_val[i]; + } + } + + if (*out != NULL) + free(*out); + *out = buf; + + ret = 0; + +done: + if (ret != 0 && buf != NULL) { + for (c = 0; buf[c] != NULL; c++) { + free(buf[c]); + } + free(buf); + } + ldap_value_free_len(vals); + return ret; +} + /* Convert an LDAP entry into an unsigned long. */ static int get_ulong(LDAP *ldp, LDAPMessage *entry, const char *name, unsigned long *out) @@ -112,6 +178,21 @@ const char *otpd_parse_user(LDAP *ldp, LDAPMessage *entry, if (i != 0 && i != ENOENT) return strerror(i); + i = get_string(ldp, entry, "ipaidpSub", + &item->user.ipaidpSub); + if (i != 0 && i != ENOENT) + return strerror(i); + + i = get_string(ldp, entry, "ipaidpConfigLink", + &item->user.ipaidpConfigLink); + if (i != 0 && i != ENOENT) + return strerror(i); + + i = get_string_array(ldp, entry, "ipauserauthtype", + &item->user.ipauserauthtypes); + if (i != 0 && i != ENOENT) + return strerror(i); + /* Get the DN. */ item->user.dn = ldap_get_dn(ldp, entry); if (item->user.dn == NULL) { @@ -122,6 +203,101 @@ const char *otpd_parse_user(LDAP *ldp, LDAPMessage *entry, return NULL; } +#define ENV_OIDC_CHILD_DEBUG_LEVEL "oidc_child_debug_level" +/* Parse the IdP configuration */ +const char *otpd_parse_idp(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item) +{ + int i; + long dbg_lvl = 0; + const char *dbg_env = NULL; + char *endptr = NULL; + + item->idp.valid = FALSE; + i = get_string(ldp, entry, "cn", &item->idp.name); + if (i != 0) { + return strerror(i); + } + + i = get_string(ldp, entry, "ipaidpIssuerURL", &item->idp.ipaidpIssuerURL); + if ((i != 0) && (i != ENOENT)) { + return strerror(i); + } + + /* We support either passing issuer URL or individual end-points */ + if (i == ENOENT) { + i = get_string(ldp, entry, "ipaidpDevAuthEndpoint", &item->idp.ipaidpDevAuthEndpoint); + if (i != 0) { + return strerror(i); + } + + i = get_string(ldp, entry, "ipaidpTokenEndpoint", &item->idp.ipaidpTokenEndpoint); + if (i != 0) { + return strerror(i); + } + + i = get_string(ldp, entry, "ipaidpUserInfoEndpoint", &item->idp.ipaidpUserInfoEndpoint); + if (i != 0) { + return strerror(i); + } + + /* JWKS end-point may be optional */ + i = get_string(ldp, entry, "ipaidpKeysEndpoint", &item->idp.ipaidpKeysEndpoint); + if ((i != 0) && (i != ENOENT)) { + return strerror(i); + } + } + + i = get_string(ldp, entry, "ipaidpClientID", &item->idp.ipaidpClientID); + if (i != 0) { + return strerror(i); + } + + i = get_string(ldp, entry, "ipaidpClientSecret", &item->idp.ipaidpClientSecret); + if ((i != 0) && (i != ENOENT)) { + return strerror(i); + } + + i = get_string(ldp, entry, "ipaidpScope", &item->idp.ipaidpScope); + if ((i != 0) && (i != ENOENT)) { + return strerror(i); + } + + i = get_string(ldp, entry, "ipaidpSub", &item->idp.ipaidpSub); + if ((i != 0) && (i != ENOENT)) { + return strerror(i); + } + + item->idp.ipaidpDebugLevelStr = NULL; + item->idp.ipaidpDebugCurl = FALSE; + dbg_env = getenv(ENV_OIDC_CHILD_DEBUG_LEVEL); + if (dbg_env != NULL && *dbg_env != '\0') { + errno = 0; + dbg_lvl = strtoul(dbg_env, &endptr, 10); + if (errno == 0 && *endptr == '\0') { + if (dbg_lvl < 0) { + dbg_lvl = 0; + } else if (dbg_lvl > 10) { + dbg_lvl = 10; + } + if (asprintf(&item->idp.ipaidpDebugLevelStr, "%ld", dbg_lvl) != -1) { + if (dbg_lvl > 5) { + item->idp.ipaidpDebugCurl = TRUE; + } + } else { + otpd_log_req(item->req, "Failed to copy debug level"); + } + } else { + otpd_log_req(item->req, + "Cannot parse value [%s] from environment variable [%s]", + dbg_env, ENV_OIDC_CHILD_DEBUG_LEVEL); + } + } + + item->idp.valid = TRUE; + return NULL; +} + /* Parse the user's RADIUS configuration. */ const char *otpd_parse_radius(LDAP *ldp, LDAPMessage *entry, struct otpd_queue_item *item) diff --git a/daemons/ipa-otpd/query.c b/daemons/ipa-otpd/query.c index 50e156033..e6f717655 100644 --- a/daemons/ipa-otpd/query.c +++ b/daemons/ipa-otpd/query.c @@ -31,6 +31,7 @@ #define _GNU_SOURCE 1 /* for asprintf() */ #include "internal.h" #include +#include #define DEFAULT_TIMEOUT 15 #define DEFAULT_RETRIES 3 @@ -39,6 +40,9 @@ static char *user[] = { "uid", "ipatokenRadiusUserName", "ipatokenRadiusConfigLink", + "ipaidpSub", + "ipaidpConfigLink", + "ipauserauthtype", NULL }; @@ -51,6 +55,37 @@ static char *radius[] = { NULL }; +static char *idp[] = { + "ipaidpClientID", + "ipaidpClientSecret", + "ipaidpIssuerURL", + "ipaidpDevAuthEndpoint", + "ipaidpTokenEndpoint", + "ipaidpUserInfoEndpoint", + "ipaidpKeysEndpoint", + "ipaidpScope", + "ipaidpSub", + "cn", + NULL +}; + +static bool auth_type_is(char **auth_types, const char *check) +{ + size_t c; + + if (auth_types == NULL || check == NULL) { + return false; + } + + for(c = 0; auth_types[c] != NULL; c++) { + if (strcasecmp(auth_types[c], check) == 0) { + return true; + } + } + + return false; +} + /* Send queued LDAP requests to the server. */ static void on_query_writable(verto_ctx *vctx, verto_ev *ev) { @@ -76,6 +111,7 @@ static void on_query_writable(verto_ctx *vctx, verto_ev *ev) goto error; otpd_log_req(item->req, "user query start"); + item->ldap_query = LDAP_QUERY_USER; if (asprintf(&filter, "(&(objectClass=Person)(krbPrincipalName=%*s))", princ->length, princ->data) < 0) @@ -86,9 +122,20 @@ static void on_query_writable(verto_ctx *vctx, verto_ev *ev) NULL, NULL, 1, &item->msgid); free(filter); + } else if (auth_type_is(item->user.ipauserauthtypes, "idp")) { + otpd_log_req(item->req, "idp query start: %s", + item->user.ipaidpConfigLink); + item->ldap_query = LDAP_QUERY_IDP; + + i = ldap_search_ext(verto_get_private(ev), + item->user.ipaidpConfigLink, + LDAP_SCOPE_BASE, NULL, idp, 0, NULL, + NULL, NULL, 1, &item->msgid); + } else if (item->radius.ipatokenRadiusSecret == NULL) { otpd_log_req(item->req, "radius query start: %s", item->user.ipatokenRadiusConfigLink); + item->ldap_query = LDAP_QUERY_RADIUS; i = ldap_search_ext(verto_get_private(ev), item->user.ipatokenRadiusConfigLink, @@ -98,6 +145,7 @@ static void on_query_writable(verto_ctx *vctx, verto_ev *ev) } else if (item->radius.ipatokenUserMapAttribute != NULL) { otpd_log_req(item->req, "username query start: %s", item->radius.ipatokenUserMapAttribute); + item->ldap_query = LDAP_QUERY_RADIUS_USERMAP; attrs[0] = item->radius.ipatokenUserMapAttribute; attrs[1] = NULL; @@ -107,7 +155,6 @@ static void on_query_writable(verto_ctx *vctx, verto_ev *ev) } if (i == LDAP_SUCCESS) { - item->sent++; push = &ctx.query.responses; } @@ -115,6 +162,78 @@ error: otpd_queue_push(push, item); } +static enum oauth2_state get_oauth2_state(enum ldap_query ldap_query, + struct otpd_queue_item *item) +{ + const krb5_data *data_pwd; + const krb5_data *data_state; + enum oauth2_state oauth2_state = OAUTH2_NO; + + data_pwd = krad_packet_get_attr(item->req, + krad_attr_name2num("User-Password"), 0); + data_state = krad_packet_get_attr(item->req, + krad_attr_name2num("Proxy-State"), 0); + + if (data_pwd == NULL && data_state == NULL) { + oauth2_state = OAUTH2_GET_DEVICE_CODE; + } else if (data_pwd == NULL && data_state != NULL) { + oauth2_state = OAUTH2_GET_ACCESS_TOKEN; + } + + /* Looks like caller does not expect oauth2 authentication */ + if (oauth2_state == OAUTH2_NO) { + return oauth2_state; + } + + if (ldap_query == LDAP_QUERY_USER) { + /* Check the user entry for required attributes */ + if (item->user.ipaidpSub == NULL) { + oauth2_state = OAUTH2_NO; + otpd_log_req(item->req, + "OAuth2 not possible, Missing 'sub' in user entry"); + } + if (item->user.ipaidpConfigLink == NULL) { + oauth2_state = OAUTH2_NO; + otpd_log_req(item->req, + "OAuth2 not possible, Missing issuer in user entry"); + } + + if (oauth2_state != OAUTH2_NO) { + /* Next step is to lookup IdP data */ + oauth2_state = OAUTH2_GET_ISSUER; + } + } else if (ldap_query == LDAP_QUERY_IDP) { + /* Check the idp entry for required attributes */ + if (item->idp.ipaidpIssuerURL == NULL) { + if (item->idp.ipaidpDevAuthEndpoint == NULL) { + oauth2_state = OAUTH2_NO; + otpd_log_req(item->req, + "OAuth2 not possible, " + "Missing authentication end-point in idp entry"); + } + if (item->idp.ipaidpTokenEndpoint == NULL) { + oauth2_state = OAUTH2_NO; + otpd_log_req(item->req, + "OAuth2 not possible, " + "Missing access token end-point in idp entry"); + } + if (item->idp.ipaidpUserInfoEndpoint == NULL) { + oauth2_state = OAUTH2_NO; + otpd_log_req(item->req, + "OAuth2 not possible, " + "Missing userinfo end-point in idp entry"); + } + } + if (item->idp.ipaidpClientID == NULL) { + oauth2_state = OAUTH2_NO; + otpd_log_req(item->req, + "OAuth2 not possible, Missing client ID in idp entry"); + } + } + + return oauth2_state; +} + /* Read LDAP responses from the server. */ static void on_query_readable(verto_ctx *vctx, verto_ev *ev) { @@ -126,6 +245,7 @@ static void on_query_readable(verto_ctx *vctx, verto_ev *ev) LDAP *ldp; int i; (void)vctx; + enum oauth2_state oauth2_state; ldp = verto_get_private(ev); @@ -150,16 +270,19 @@ static void on_query_readable(verto_ctx *vctx, verto_ev *ev) goto egress; err = NULL; - switch (item->sent) { - case 1: + switch (item->ldap_query) { + case LDAP_QUERY_USER: err = otpd_parse_user(ldp, entry, item); break; - case 2: + case LDAP_QUERY_RADIUS: err = otpd_parse_radius(ldp, entry, item); break; - case 3: + case LDAP_QUERY_RADIUS_USERMAP: err = otpd_parse_radius_username(ldp, entry, item); break; + case LDAP_QUERY_IDP: + err = otpd_parse_idp(ldp, entry, item); + break; default: ldap_msgfree(entry); goto egress; @@ -181,14 +304,14 @@ static void on_query_readable(verto_ctx *vctx, verto_ev *ev) item->msgid = -1; - switch (item->sent) { - case 1: + switch (item->ldap_query) { + case LDAP_QUERY_USER: otpd_log_req(item->req, "user query end: %s", item->error == NULL ? item->user.dn : item->error); if (item->user.dn == NULL || item->user.uid == NULL) goto egress; break; - case 2: + case LDAP_QUERY_RADIUS: otpd_log_req(item->req, "radius query end: %s", item->error == NULL ? item->radius.ipatokenRadiusServer @@ -197,22 +320,47 @@ static void on_query_readable(verto_ctx *vctx, verto_ev *ev) item->radius.ipatokenRadiusSecret == NULL) goto egress; break; - case 3: + case LDAP_QUERY_RADIUS_USERMAP: otpd_log_req(item->req, "username query end: %s", item->error == NULL ? item->user.other : item->error); break; + case LDAP_QUERY_IDP: + otpd_log_req(item->req, "idp query end: %s", + item->error == NULL ? item->idp.name : item->error); + if (!item->idp.valid) { + goto egress; + } + break; default: goto egress; } - if (item->error != NULL) - goto egress; - - if (item->sent == 1 && item->user.ipatokenRadiusConfigLink != NULL) { + /* Check for oauth2 */ + oauth2_state = get_oauth2_state(item->ldap_query, item); + if (oauth2_state == OAUTH2_GET_ISSUER) { push = &ctx.query.requests; event = ctx.query.io; goto egress; - } else if (item->sent == 2 && + } else if (oauth2_state != OAUTH2_NO) { + i = oauth2(&item, oauth2_state); + if (i != 0) { + goto egress; + } else { + /* oauth2 will call ctx.stdio.writer, so we can return here */ + return; + } + } + + if (item->error != NULL) + goto egress; + + if (item->ldap_query == LDAP_QUERY_USER && + item->user.ipatokenRadiusConfigLink != NULL) { + + push = &ctx.query.requests; + event = ctx.query.io; + goto egress; + } else if (item->ldap_query == LDAP_QUERY_RADIUS && item->radius.ipatokenUserMapAttribute != NULL && item->user.ipatokenRadiusUserName == NULL) { push = &ctx.query.requests; diff --git a/daemons/ipa-otpd/queue.c b/daemons/ipa-otpd/queue.c index 2944b7ea0..21c7e03b2 100644 --- a/daemons/ipa-otpd/queue.c +++ b/daemons/ipa-otpd/queue.c @@ -46,6 +46,8 @@ krb5_error_code otpd_queue_item_new(krad_packet *req, void otpd_queue_item_free(struct otpd_queue_item *item) { + size_t c; + if (item == NULL) return; @@ -54,9 +56,34 @@ void otpd_queue_item_free(struct otpd_queue_item *item) free(item->user.ipatokenRadiusUserName); free(item->user.ipatokenRadiusConfigLink); free(item->user.other); + free(item->user.ipaidpSub); + free(item->user.ipaidpConfigLink); + if (item->user.ipauserauthtypes != NULL) { + for (c = 0; item->user.ipauserauthtypes[c] != NULL; c++) { + free(item->user.ipauserauthtypes[c]); + } + free(item->user.ipauserauthtypes); + } free(item->radius.ipatokenRadiusServer); free(item->radius.ipatokenRadiusSecret); free(item->radius.ipatokenUserMapAttribute); + free(item->idp.ipaidpIssuerURL); + free(item->idp.ipaidpDevAuthEndpoint); + free(item->idp.ipaidpTokenEndpoint); + free(item->idp.ipaidpUserInfoEndpoint); + free(item->idp.ipaidpKeysEndpoint); + free(item->idp.name); + free(item->idp.ipaidpClientID); + if (item->idp.ipaidpClientSecret != NULL) { + size_t len = strlen(item->idp.ipaidpClientSecret); + (void*) memset(item->idp.ipaidpClientSecret, 0, len); + free(item->idp.ipaidpClientSecret); + } + free(item->idp.ipaidpScope); + free(item->idp.ipaidpSub); + free(item->idp.ipaidpDebugLevelStr); + free(item->oauth2.device_code_reply); + free(item->oauth2.state.data); free(item->error); krad_packet_free(item->req); krad_packet_free(item->rsp); diff --git a/daemons/ipa-otpd/stdio.c b/daemons/ipa-otpd/stdio.c index ac51c78d3..f652b3a46 100644 --- a/daemons/ipa-otpd/stdio.c +++ b/daemons/ipa-otpd/stdio.c @@ -166,6 +166,7 @@ void otpd_on_stdout_writable(verto_ctx *vctx, verto_ev *ev) /* Send the packet. */ data = krad_packet_encode(item->rsp); + otpd_log_req(item->req, "sent: %d data: %d", item->sent, data->length); i = write(verto_get_fd(ev), data->data + item->sent, data->length - item->sent); if (i < 0) { @@ -191,6 +192,7 @@ void otpd_on_stdout_writable(verto_ctx *vctx, verto_ev *ev) /* If the packet was completely sent, free the response. */ item->sent += i; + otpd_log_req(item->req, "..sent: %d data: %d", item->sent, data->length); if (item->sent == data->length) { otpd_log_req(item->req, "response sent: %s", krad_code_num2name(krad_packet_get_code(item->rsp)));