From 597043ae4811d685b23a3b4b792f11491315c592 Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:10:17 +0300 Subject: [PATCH 1/7] add auth-oidc plugin --- configure.ac | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 5a64f2d13..e56207e01 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -dnl Copyright (C) 2004-2023 Alexander Chernov +dnl Copyright (C) 2004-2024 Alexander Chernov AC_INIT([ejudge],[3]) AC_PREREQ([2.71]) @@ -1453,7 +1453,7 @@ AC_CONFIG_SUBDIRS([libdwarf]) AC_CONFIG_SUBDIRS([libbacktrace]) -AC_CONFIG_FILES([Makefile extra/Makefile extra/captest/Makefile checkers/Makefile scripts/Makefile ejudge-config.v scripts/festival plugins/common-mysql/Makefile plugins/userlist-mysql/Makefile plugins/clardb-mysql/Makefile plugins/rundb-mysql/Makefile plugins/common-mongo/Makefile plugins/xuser-mongo/Makefile style/ejudge-upgrade-web cfront/Makefile reuse/Makefile csp/contests/Makefile csp/super-server/Makefile csp_header.make plugins/telegram/Makefile plugins/avatar-mongo/Makefile plugins/status-mongo/Makefile plugins/status-mysql/Makefile plugins/auth-google/Makefile plugins/auth-base/Makefile plugins/auth-vk/Makefile plugins/auth-fb/Makefile plugins/xuser-mysql/Makefile plugins/avatar-mysql/Makefile plugins/variant-mysql/Makefile plugins/storage-mysql/Makefile plugins/cache-mysql/Makefile plugins/submit-mysql/Makefile plugins/userprob-mysql/Makefile plugins/vcs-gitlab/Makefile plugins/auth-yandex/Makefile plugins/notify-redis/Makefile]) +AC_CONFIG_FILES([Makefile extra/Makefile extra/captest/Makefile checkers/Makefile scripts/Makefile ejudge-config.v scripts/festival plugins/common-mysql/Makefile plugins/userlist-mysql/Makefile plugins/clardb-mysql/Makefile plugins/rundb-mysql/Makefile plugins/common-mongo/Makefile plugins/xuser-mongo/Makefile style/ejudge-upgrade-web cfront/Makefile reuse/Makefile csp/contests/Makefile csp/super-server/Makefile csp_header.make plugins/telegram/Makefile plugins/avatar-mongo/Makefile plugins/status-mongo/Makefile plugins/status-mysql/Makefile plugins/auth-google/Makefile plugins/auth-base/Makefile plugins/auth-vk/Makefile plugins/auth-fb/Makefile plugins/xuser-mysql/Makefile plugins/avatar-mysql/Makefile plugins/variant-mysql/Makefile plugins/storage-mysql/Makefile plugins/cache-mysql/Makefile plugins/submit-mysql/Makefile plugins/userprob-mysql/Makefile plugins/vcs-gitlab/Makefile plugins/auth-yandex/Makefile plugins/notify-redis/Makefile plugins/auth-oidc/Makefile]) AC_OUTPUT #cp -p config.h include/reuse From 74a53b4bee45a0a4027150ce00b2b5eed3305e6e Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:10:59 +0300 Subject: [PATCH 2/7] regenerate configure --- configure | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configure b/configure index 80c68ef7c..ea11887f9 100755 --- a/configure +++ b/configure @@ -9033,7 +9033,7 @@ subdirs="$subdirs libdwarf" subdirs="$subdirs libbacktrace" -ac_config_files="$ac_config_files Makefile extra/Makefile extra/captest/Makefile checkers/Makefile scripts/Makefile ejudge-config.v scripts/festival plugins/common-mysql/Makefile plugins/userlist-mysql/Makefile plugins/clardb-mysql/Makefile plugins/rundb-mysql/Makefile plugins/common-mongo/Makefile plugins/xuser-mongo/Makefile style/ejudge-upgrade-web cfront/Makefile reuse/Makefile csp/contests/Makefile csp/super-server/Makefile csp_header.make plugins/telegram/Makefile plugins/avatar-mongo/Makefile plugins/status-mongo/Makefile plugins/status-mysql/Makefile plugins/auth-google/Makefile plugins/auth-base/Makefile plugins/auth-vk/Makefile plugins/auth-fb/Makefile plugins/xuser-mysql/Makefile plugins/avatar-mysql/Makefile plugins/variant-mysql/Makefile plugins/storage-mysql/Makefile plugins/cache-mysql/Makefile plugins/submit-mysql/Makefile plugins/userprob-mysql/Makefile plugins/vcs-gitlab/Makefile plugins/auth-yandex/Makefile plugins/notify-redis/Makefile" +ac_config_files="$ac_config_files Makefile extra/Makefile extra/captest/Makefile checkers/Makefile scripts/Makefile ejudge-config.v scripts/festival plugins/common-mysql/Makefile plugins/userlist-mysql/Makefile plugins/clardb-mysql/Makefile plugins/rundb-mysql/Makefile plugins/common-mongo/Makefile plugins/xuser-mongo/Makefile style/ejudge-upgrade-web cfront/Makefile reuse/Makefile csp/contests/Makefile csp/super-server/Makefile csp_header.make plugins/telegram/Makefile plugins/avatar-mongo/Makefile plugins/status-mongo/Makefile plugins/status-mysql/Makefile plugins/auth-google/Makefile plugins/auth-base/Makefile plugins/auth-vk/Makefile plugins/auth-fb/Makefile plugins/xuser-mysql/Makefile plugins/avatar-mysql/Makefile plugins/variant-mysql/Makefile plugins/storage-mysql/Makefile plugins/cache-mysql/Makefile plugins/submit-mysql/Makefile plugins/userprob-mysql/Makefile plugins/vcs-gitlab/Makefile plugins/auth-yandex/Makefile plugins/notify-redis/Makefile plugins/auth-oidc/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -9761,6 +9761,7 @@ do "plugins/vcs-gitlab/Makefile") CONFIG_FILES="$CONFIG_FILES plugins/vcs-gitlab/Makefile" ;; "plugins/auth-yandex/Makefile") CONFIG_FILES="$CONFIG_FILES plugins/auth-yandex/Makefile" ;; "plugins/notify-redis/Makefile") CONFIG_FILES="$CONFIG_FILES plugins/notify-redis/Makefile" ;; + "plugins/auth-oidc/Makefile") CONFIG_FILES="$CONFIG_FILES plugins/auth-oidc/Makefile" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; esac From 319129b014d9cd5a74941d9a33d6f2a0d68236ac Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:12:38 +0300 Subject: [PATCH 3/7] add auth-oidc plugin --- main.unix.make | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main.unix.make b/main.unix.make index b763657b5..dc4056f8e 100644 --- a/main.unix.make +++ b/main.unix.make @@ -1,6 +1,6 @@ # -*- Makefile -*- -# Copyright (C) 2014-2023 Alexander Chernov */ +# Copyright (C) 2014-2024 Alexander Chernov */ # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -224,6 +224,7 @@ subdirs_all: $(MAKE) -C plugins/telegram DESTDIR="${DESTDIR}" all $(MAKE) -C plugins/auth-base DESTDIR="${DESTDIR}" all $(MAKE) -C plugins/auth-google DESTDIR="${DESTDIR}" all + $(MAKE) -C plugins/auth-oidc DESTDIR="${DESTDIR}" all $(MAKE) -C plugins/auth-vk DESTDIR="${DESTDIR}" all $(MAKE) -C plugins/auth-yandex DESTDIR="${DESTDIR}" all $(MAKE) -C plugins/notify-redis DESTDIR="${DESTDIR}" all @@ -307,6 +308,7 @@ install: local_install $(MAKE) -C plugins/telegram DESTDIR="${DESTDIR}" install $(MAKE) -C plugins/auth-base DESTDIR="${DESTDIR}" install $(MAKE) -C plugins/auth-google DESTDIR="${DESTDIR}" install + $(MAKE) -C plugins/auth-oidc DESTDIR="${DESTDIR}" install $(MAKE) -C plugins/auth-vk DESTDIR="${DESTDIR}" install $(MAKE) -C plugins/auth-yandex DESTDIR="${DESTDIR}" install $(MAKE) -C plugins/notify-redis DESTDIR="${DESTDIR}" install @@ -519,6 +521,7 @@ subdir_clean: $(MAKE) -C plugins/telegram DESTDIR="${DESTDIR}" clean $(MAKE) -C plugins/auth-base DESTDIR="${DESTDIR}" clean $(MAKE) -C plugins/auth-google DESTDIR="${DESTDIR}" clean + $(MAKE) -C plugins/auth-oidc DESTDIR="${DESTDIR}" clean $(MAKE) -C plugins/auth-vk DESTDIR="${DESTDIR}" clean $(MAKE) -C plugins/auth-yandex DESTDIR="${DESTDIR}" clean $(MAKE) -C plugins/notify-redis DESTDIR="${DESTDIR}" clean @@ -558,6 +561,7 @@ subdir_distclean : $(MAKE) -C plugins/telegram DESTDIR="${DESTDIR}" distclean $(MAKE) -C plugins/auth-base DESTDIR="${DESTDIR}" distclean $(MAKE) -C plugins/auth-google DESTDIR="${DESTDIR}" distclean + $(MAKE) -C plugins/auth-oidc DESTDIR="${DESTDIR}" distclean $(MAKE) -C plugins/auth-vk DESTDIR="${DESTDIR}" distclean $(MAKE) -C plugins/auth-yandex DESTDIR="${DESTDIR}" distclean $(MAKE) -C plugins/notify-redis DESTDIR="${DESTDIR}" distclean From fe24f29bc09d29e8e6ccec552b08fc256109574c Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:19:50 +0300 Subject: [PATCH 4/7] implement OIDC plugin --- plugins/auth-oidc/Makefile.in | 55 ++++ plugins/auth-oidc/auth_oidc.c | 559 ++++++++++++++++++++++++++++++++++ plugins/auth-oidc/empty.make | 19 ++ plugins/auth-oidc/main.make | 49 +++ 4 files changed, 682 insertions(+) create mode 100644 plugins/auth-oidc/Makefile.in create mode 100644 plugins/auth-oidc/auth_oidc.c create mode 100644 plugins/auth-oidc/empty.make create mode 100644 plugins/auth-oidc/main.make diff --git a/plugins/auth-oidc/Makefile.in b/plugins/auth-oidc/Makefile.in new file mode 100644 index 000000000..44c1f9345 --- /dev/null +++ b/plugins/auth-oidc/Makefile.in @@ -0,0 +1,55 @@ +# -*- Makefile -*- +# @configure_input@ + +# Copyright (C) 2024 Alexander Chernov */ + +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 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 +# Lesser General Public License for more details. + +prefix=@prefix@ +exec_prefix=@exec_prefix@ +bindir=@bindir@ +datarootdir=@datarootdir@ +datadir=@datadir@ +includedir=@includedir@ +libdir=@libdir@ +libexecdir=@libexecdir@ + +EXPAT_DIR=@ac_cv_expat_root@ +EXPAT_INCL_OPT=@ac_cv_expat_include_opt@ +EXPAT_LIB_OPT=@ac_cv_expat_lib_opt@ + +MYSQL_DIR=@ac_cv_mysql_root@ +MYSQL_INCL_OPT=@ac_cv_mysql_include_opt@ +MYSQL_LIB_OPT=@ac_cv_mysql_lib_opt@ +MYSQL_LIBS=@ac_cv_mysql_libs@ + +WPTRSIGN=@ac_cv_gcc_wno_pointer_sign@ @ac_cv_gcc_wno_format_truncation@ +WERROR=@ac_cv_werror_flag@ + +ifdef RELEASE +CDEBUGFLAGS=-O2 -Wall -DNDEBUG -DRELEASE ${WERROR} +else +CDEBUGFLAGS=-g -Wall ${WERROR} -O +endif +ifdef STATIC +CDEBUGFLAGS += -static +endif +CEXTRAFLAGS= +LDEXTRAFLAGS= +EXTRALIBS= +CCOMPFLAGS=-D_GNU_SOURCE +LDCOMPFLAGS= + +ifeq ($(MYSQL_LIBS),) +include empty.make +else +include main.make +endif diff --git a/plugins/auth-oidc/auth_oidc.c b/plugins/auth-oidc/auth_oidc.c new file mode 100644 index 000000000..f4de74fae --- /dev/null +++ b/plugins/auth-oidc/auth_oidc.c @@ -0,0 +1,559 @@ +/* -*- mode: c; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2024 Alexander Chernov */ + +/* + * 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 2 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. + */ + +#include "ejudge/config.h" +#include "ejudge/auth_plugin.h" +#include "ejudge/xml_utils.h" +#include "ejudge/xalloc.h" +#include "ejudge/errlog.h" +#include "ejudge/logger.h" +#include "ejudge/cJSON.h" +#include "ejudge/base64.h" +#include "ejudge/random.h" +#include "ejudge/misctext.h" +#include "ejudge/auth_base_plugin.h" + +#if CONF_HAS_LIBCURL - 0 == 1 +#include +#else +#error curl required +#endif + +struct auth_oidc_state +{ + struct auth_base_plugin_iface *bi; + struct auth_base_plugin_state *bd; + + CURL *curl; + unsigned char *configuration_endpoint; + unsigned char *authorization_endpoint; + unsigned char *token_endpoint; + unsigned char *scope; + + unsigned char *client_id; + unsigned char *client_secret; + unsigned char *redirect_uri; + + auth_set_command_handler_t set_command_handler_func; + void *set_command_handler_data; + + auth_send_job_handler_t send_job_handler_func; + void *send_job_handler_data; +}; + +static struct common_plugin_data* +init_func(void) +{ + struct auth_oidc_state *state; + + XCALLOC(state, 1); + + state->curl = curl_easy_init(); + curl_easy_setopt(state->curl, CURLOPT_NOSIGNAL, 1L); + + return (struct common_plugin_data*) state; +} + +static int +finish_func(struct common_plugin_data *data) +{ + return 0; +} + +static int +prepare_func( + struct common_plugin_data *data, + const struct ejudge_cfg *config, + struct xml_tree *tree) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + __attribute__((unused)) const struct xml_parse_spec *spec = ejudge_cfg_get_spec(); + + // load auth base plugin + const struct common_loaded_plugin *mplg; + if (!(mplg = plugin_load_external(0, "auth", "base", config))) { + err("cannot load auth_base plugin"); + return -1; + } + state->bi = (struct auth_base_plugin_iface *) mplg->iface; + state->bd = (struct auth_base_plugin_state *) mplg->data; + + // handle config section + ASSERT(tree->tag == spec->default_elem); + ASSERT(!strcmp(tree->name[0], "config")); + + for (struct xml_tree *p = tree->first_down; p; p = p->right) { + ASSERT(p->tag == spec->default_elem); + + if (!strcmp(p->name[0], "client_id")) { + if (xml_leaf_elem(p, &state->client_id, 1, 0) < 0) return -1; + } else if (!strcmp(p->name[0], "client_secret")) { + if (xml_leaf_elem(p, &state->client_secret, 1, 0) < 0) return -1; + } else if (!strcmp(p->name[0], "redirect_uri")) { + if (xml_leaf_elem(p, &state->redirect_uri, 1, 0) < 0) return -1; + } else if (!strcmp(p->name[0], "configuration_endpoint")) { + if (xml_leaf_elem(p, &state->configuration_endpoint, 1, 0) < 0) return -1; + } else if (!strcmp(p->name[0], "scope")) { + if (xml_leaf_elem(p, &state->scope, 1, 0) < 0) return -1; + } + } + + return 0; +} + +static int +open_func(void *data) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + if (state->bi->open(state->bd) < 0) + return 1; + + return 0; +} + +static int +fetch_oidc_endpoints(struct auth_oidc_state *state) +{ + char *page_text = NULL; + size_t page_size = 0; + FILE *file = NULL; + CURLcode res = 0; + cJSON *root = NULL; + + curl_easy_reset(state->curl); + curl_easy_setopt(state->curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(state->curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(state->curl, CURLOPT_COOKIEFILE, ""); + curl_easy_setopt(state->curl, CURLOPT_URL, state->configuration_endpoint); + file = open_memstream(&page_text, &page_size); + curl_easy_setopt(state->curl, CURLOPT_WRITEFUNCTION, NULL); + curl_easy_setopt(state->curl, CURLOPT_WRITEDATA, file); + res = curl_easy_perform(state->curl); + fclose(file); file = NULL; + if (res != CURLE_OK) { + err("Request failed: %s", curl_easy_strerror(res)); + goto fail; + } + root = cJSON_Parse(page_text); + free(page_text); page_text = NULL; + if (!root) { + err("JSON parse failed"); + goto fail; + } + if (root->type != cJSON_Object) { + err("invalid json, root document expected"); + goto fail; + } + cJSON *jauth = cJSON_GetObjectItem(root, "authorization_endpoint"); + if (!jauth || jauth->type != cJSON_String) { + err("invalid json, invalid authorization_endpoint"); + goto fail; + } + state->authorization_endpoint = xstrdup(jauth->valuestring); + + cJSON *jtoken = cJSON_GetObjectItem(root, "token_endpoint"); + if (!jtoken || jtoken->type != cJSON_String) { + err("invalid json, invalid token_endpoint"); + goto fail; + } + state->token_endpoint = xstrdup(jtoken->valuestring); + cJSON_Delete(root); + + return 0; + +fail: + if (root) cJSON_Delete(root); + if (file) fclose(file); + free(page_text); + return -1; +} + +static int +check_func(void *data) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + if (state->bi->check(state->bd) < 0) + return -1; + + fetch_oidc_endpoints(state); + + return 0; +} + +static void +set_set_command_handler_func( + void *data, + auth_set_command_handler_t setter, + void *setter_self) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + state->set_command_handler_func = setter; + state->set_command_handler_data = setter_self; +} + +static void +set_send_job_handler_func( + void *data, + auth_send_job_handler_t handler, + void *handler_self) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + state->send_job_handler_func = handler; + state->send_job_handler_data = handler_self; +} + +/* + args[0] = "auth_oidc" + args[1] = request_id + args[2] = request_code + args[3] = NULL; + */ +static void +packet_handler_auth_oidc(int uid, int argc, char **argv, void *user) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) user; + + const unsigned char *request_id = argv[1]; + const unsigned char *request_code = argv[2]; + + char *post_s = NULL; + size_t post_z = 0; + FILE *post_f = NULL; + char *json_s = NULL; + size_t json_z = 0; + FILE *json_f = NULL; + struct html_armor_buffer ab = HTML_ARMOR_INITIALIZER; + CURLcode res = 0; + int request_status = 2; // failed + const char *error_message = "unknown error"; + const unsigned char *response_email = NULL; + const unsigned char *response_name = NULL; + const unsigned char *access_token = NULL; + const unsigned char *id_token = NULL; + cJSON *root = NULL; + cJSON *jwt = NULL; + unsigned char *jwt_payload = NULL; + + post_f = open_memstream(&post_s, &post_z); + fprintf(post_f, "grant_type=authorization_code"); + fprintf(post_f, "&code=%s", url_armor_buf(&ab, request_code)); + fprintf(post_f, "&client_id=%s", url_armor_buf(&ab, state->client_id)); + fprintf(post_f, "&client_secret=%s", url_armor_buf(&ab, state->client_secret)); + fprintf(post_f, "&redirect_uri=%s/S4", url_armor_buf(&ab, state->redirect_uri)); + fclose(post_f); post_f = NULL; + + json_f = open_memstream(&json_s, &json_z); + curl_easy_reset(state->curl); + curl_easy_setopt(state->curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(state->curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(state->curl, CURLOPT_COOKIEFILE, ""); + curl_easy_setopt(state->curl, CURLOPT_URL, state->token_endpoint); + curl_easy_setopt(state->curl, CURLOPT_POST, 1); + curl_easy_setopt(state->curl, CURLOPT_POSTFIELDS, post_s); + curl_easy_setopt(state->curl, CURLOPT_WRITEFUNCTION, NULL); + curl_easy_setopt(state->curl, CURLOPT_WRITEDATA, json_f); + res = curl_easy_perform(state->curl); + fclose(json_f); json_f = NULL; + free(post_s); post_s = NULL; post_z = 0; + if (res != CURLE_OK) { + err("Request failed: %s", curl_easy_strerror(res)); + error_message = "request failed"; + goto done; + } + + fprintf(stderr, "oidc json: >>%s<<\n", json_s); + + if (!(root = cJSON_Parse(json_s))) { + error_message = "oidc JSON parse failed"; + goto done; + } + free(json_s); json_s = NULL; json_z = 0; + + if (root->type != cJSON_Object) { + error_message = "oidc root document expected"; + goto done; + } + + cJSON *j = cJSON_GetObjectItem(root, "access_token"); + if (!j || j->type != cJSON_String) { + error_message = "invalid oidc json: access_token"; + goto done; + } + access_token = j->valuestring; + if (!(j = cJSON_GetObjectItem(root, "id_token")) || j->type != cJSON_String) { + error_message = "invalid oidc json: id_token"; + goto done; + } + id_token = j->valuestring; + + // parse payload of JWT + { + char *p1 = strchr(id_token, '.'); + if (!p1) { + error_message = "invalid oidc json: invalid JWT (1)"; + goto done; + } + char *p2 = strchr(p1 + 1, '.'); + if (!p2) { + error_message = "invalid oidc json: invalid JWT (2)"; + goto done; + } + + jwt_payload = xmalloc(strlen(id_token) + 1); + int err = 0; + int len = base64u_decode(p1 + 1, p2 - p1 - 1, jwt_payload, &err); + if (err) { + error_message = "invalid oidc json: base64u payload decode error"; + goto done; + } + jwt_payload[len] = 0; + } + + if (!(jwt = cJSON_Parse(jwt_payload))) { + error_message = "JWT payload parse failed"; + goto done; + } + if (jwt->type != cJSON_Object) { + error_message = "JWT payload root document expected"; + goto done; + } + + if (!(j = cJSON_GetObjectItem(jwt, "email")) || j->type != cJSON_String) { + error_message = "JWT payload email expected"; + goto done; + } + response_email = j->valuestring; + + if ((j = cJSON_GetObjectItem(jwt, "name")) && j->type == cJSON_String) { + response_name = j->valuestring; + } + + // success + request_status = 3; + error_message = NULL; + +done:; + state->bi->update_stage2(state->bd, request_id, + request_status, error_message, + response_name, + NULL /* response_user_id */, + response_email, + access_token, id_token); + if (jwt) cJSON_Delete(jwt); + free(jwt_payload); + if (root) cJSON_Delete(root); + if (json_f) fclose(json_f); + free(json_s); + html_armor_free(&ab); + if (post_f) fclose(post_f); + free(post_s); +} + +static void +queue_packet_handler_auth_oidc(int uid, int argc, char **argv, void *user) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) user; + state->bi->enqueue_action(state->bd, packet_handler_auth_oidc, uid, argc, argv, user); +} + +static int +start_thread_func(void *data) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + if (!state->set_command_handler_func) { + return 0; + } + + state->set_command_handler_func(state->set_command_handler_data, + "auth_oidc", + queue_packet_handler_auth_oidc, + data); + + int r = state->bi->start_thread(state->bd); + return r; +} + +static unsigned char * +get_redirect_url_func( + void *data, + const unsigned char *cookie, + const unsigned char *provider, + const unsigned char *role, + int contest_id, + const unsigned char *extra_data) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + unsigned char rbuf[16]; + unsigned char ebuf[32]; + time_t create_time = time(NULL); + time_t expiry_time = create_time + 60; + char *url_s = NULL; + size_t url_z = 0; + FILE *url_f = NULL; + struct html_armor_buffer ab = HTML_ARMOR_INITIALIZER; + + random_init(); + random_bytes(rbuf, sizeof(rbuf)); + int len = base64u_encode(rbuf, sizeof(rbuf), ebuf); + ebuf[len] = 0; + + if (state->bi->insert_stage1(state->bd, + ebuf, provider, role, cookie, contest_id, + extra_data, create_time, expiry_time) < 0) { + goto fail; + } + + url_f = open_memstream(&url_s, &url_z); + fprintf(url_f, "%s?client_id=%s&response_type=code", + state->authorization_endpoint, + url_armor_buf(&ab, state->client_id)); + fprintf(url_f, "&redirect_uri=%s/S4", url_armor_buf(&ab, state->redirect_uri)); + fprintf(url_f, "&state=%s", ebuf); + if (state->scope && state->scope[0]) { + fprintf(url_f, "&scope=%s", url_armor_buf(&ab, state->scope)); + } else { + fprintf(url_f, "&scope=openid%%20profile%%20email"); + } + fclose(url_f); url_f = NULL; + + html_armor_free(&ab); + return url_s; + +fail:; + html_armor_free(&ab); + return NULL; +} + +static unsigned char * +process_auth_callback_func( + void *data, + const unsigned char *state_id, + const unsigned char *code) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + struct oauth_stage1_internal oas1 = {}; + struct oauth_stage2_internal oas2 = {}; + unsigned char rbuf[16]; + unsigned char ebuf[32] = {}; + + if (state->bi->extract_stage1(state->bd, state_id, &oas1) <= 0) { + goto fail; + } + + random_init(); + random_bytes(rbuf, sizeof(rbuf)); + int len = base64u_encode(rbuf, sizeof(rbuf), ebuf); + ebuf[len] = 0; + + oas2.request_id = xstrdup(ebuf); + oas2.request_code = xstrdup(code); + oas2.cookie = oas1.cookie; oas1.cookie = NULL; + oas2.provider = oas1.provider; oas1.provider = NULL; + oas2.role = oas1.role; oas1.role = NULL; + oas2.contest_id = oas1.contest_id; + oas2.extra_data = oas1.extra_data; oas1.extra_data = NULL; + oas2.create_time = time(NULL); + + if (state->bi->insert_stage2(state->bd, &oas2) < 0) { + goto fail; + } + + if (state->send_job_handler_func) { + unsigned char *args[] = { "auth_oidc", oas2.request_id, oas2.request_code, NULL }; + state->send_job_handler_func(state->send_job_handler_data, args); + } else { + err("send_job_handler_func is not installed"); + goto fail; + } + + state->bi->free_stage1(state->bd, &oas1); + state->bi->free_stage2(state->bd, &oas2); + + return xstrdup(ebuf); + +fail: + state->bi->free_stage1(state->bd, &oas1); + state->bi->free_stage2(state->bd, &oas2); + return NULL; +} + +static struct OAuthLoginResult +get_result_func( + void *data, + const unsigned char *request_id) +{ + struct auth_oidc_state *state = (struct auth_oidc_state*) data; + + unsigned char *error_message = NULL; + struct oauth_stage2_internal oas2 = {}; + struct OAuthLoginResult res = {}; + + if (state->bi->extract_stage2(state->bd, request_id, &oas2) <= 0) { + goto fail; + } + + res.status = oas2.request_state; + res.provider = oas2.provider; oas2.provider = NULL; + res.role = oas2.role; oas2.role = NULL; + res.cookie = oas2.cookie; oas2.cookie = NULL; + res.extra_data = oas2.extra_data; oas2.extra_data = NULL; + res.user_id = oas2.response_user_id; oas2.response_user_id = NULL; + res.email = oas2.response_email; oas2.response_email = NULL; + res.name = oas2.response_name; oas2.response_name = NULL; + res.access_token = oas2.access_token; oas2.access_token = NULL; + res.id_token = oas2.id_token; oas2.id_token = NULL; + res.error_message = oas2.error_message; oas2.error_message = NULL; + res.contest_id = oas2.contest_id; + state->bi->free_stage2(state->bd, &oas2); + + return res; +fail: + state->bi->free_stage2(state->bd, &oas2); + if (!error_message) error_message = xstrdup("unknown error"); + return (struct OAuthLoginResult) { .status = 2, .error_message = error_message }; +} + +struct auth_plugin_iface plugin_auth_oidc = +{ + { + { + sizeof (struct auth_plugin_iface), + EJUDGE_PLUGIN_IFACE_VERSION, + "auth", + "oidc", + }, + COMMON_PLUGIN_IFACE_VERSION, + init_func, + finish_func, + prepare_func, + }, + AUTH_PLUGIN_IFACE_VERSION, + open_func, + check_func, + start_thread_func, + set_set_command_handler_func, + set_send_job_handler_func, + get_redirect_url_func, + process_auth_callback_func, + get_result_func, +}; diff --git a/plugins/auth-oidc/empty.make b/plugins/auth-oidc/empty.make new file mode 100644 index 000000000..1bc1775fc --- /dev/null +++ b/plugins/auth-oidc/empty.make @@ -0,0 +1,19 @@ +# -*- Makefile -*- + +# Copyright (C) 2024 Alexander Chernov */ + +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 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 +# Lesser General Public License for more details. + +all : +install : +clean : +distclean : + -rm -f Makefile diff --git a/plugins/auth-oidc/main.make b/plugins/auth-oidc/main.make new file mode 100644 index 000000000..c086a0d09 --- /dev/null +++ b/plugins/auth-oidc/main.make @@ -0,0 +1,49 @@ +# -*- Makefile -*- + +# Copyright (C) 2024 Alexander Chernov */ + +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 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 +# Lesser General Public License for more details. + +PLUGINDIR = $(libexecdir)/ejudge/plugins + +AUTH_OIDC_CFILES = auth_oidc.c + +CFILES = $(AUTH_OIDC_CFILES) +HFILES = + +CC = gcc +LD = gcc + +CFLAGS = -I. -I../.. -I../../include $(MYSQL_INCL_OPT) $(EXPAT_INCL_OPT) $(CDEBUGFLAGS) $(CCOMPFLAGS) $(CEXTRAFLAGS) $(WPTRSIGN) +LDFLAGS = $(MYSQL_LIB_OPT) $(EXPAT_LIB_OPT) $(CDEBUGFLAGS) $(LDCOMPFLAGS) $(LDEXTRAFLAGS) +LDLIBS = $(EXTRALIBS) $(MYSQL_LIBS) -lcurl -lexpat -lm + +PLUGINS = auth_oidc.so + +all : $(PLUGINS) + +install : $(PLUGINS) + install -d "${DESTDIR}${PLUGINDIR}" + install -m 0755 $(PLUGINS) "${DESTDIR}${PLUGINDIR}" + +clean : + -rm -f *.so *.o deps.make + +distclean : clean + -rm -f Makefile + +deps.make : $(CFILES) $(HFILES) + ../../cdeps -v AUTH_OIDC_OFILES -I ../.. -I ../../include -g -c '$$(CC) $$(CFLAGS) -DPIC -fPIC' $(AUTH_OIDC_CFILES) > deps.make + +include deps.make + +auth_oidc.so : $(AUTH_OIDC_OFILES) + $(LD) -shared $(LDFLAGS) $^ -o $@ $(LDLIBS) From b141c4f275ca227ce9150e19270266a8fc27311b Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:22:55 +0300 Subject: [PATCH 5/7] implement OIDC plugin support --- bin/ej-jobs.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/bin/ej-jobs.c b/bin/ej-jobs.c index b43c7bd11..37492deee 100644 --- a/bin/ej-jobs.c +++ b/bin/ej-jobs.c @@ -1,6 +1,6 @@ /* -*- mode: c; c-basic-offset: 4 -*- */ -/* Copyright (C) 2006-2023 Alexander Chernov */ +/* Copyright (C) 2006-2024 Alexander Chernov */ /* * This program is free software; you can redistribute it and/or modify @@ -194,6 +194,10 @@ struct AppState const struct auth_plugin_iface *auth_yandex_iface; void *auth_yandex_data; + // OIDC Auth plugin + const struct auth_plugin_iface *auth_oidc_iface; + void *auth_oidc_data; + // Gitlab VCS plugin const struct vcs_plugin_iface *vcs_gitlab_iface; void *vcs_gitlab_data; @@ -1733,6 +1737,51 @@ load_auth_yandex_plugin(struct AppState *as) return 0; } +static int +load_auth_oidc_plugin(struct AppState *as) +{ + struct xml_tree *oidc_cfg = ejudge_cfg_get_plugin_config(as->config, "auth", "oidc"); + if (!oidc_cfg) return 0; + + const struct common_loaded_plugin *oidc_plugin = plugin_load_external(NULL, "auth", "oidc", as->config); + if (!oidc_plugin) { + err("failed to load auth_oidc plugin"); + return -1; + } + + if (oidc_plugin->iface->b.size != sizeof(struct auth_plugin_iface)) { + err("auth_oidc plugin interface size mismatch"); + return -1; + } + + const struct auth_plugin_iface *auth_iface = (const struct auth_plugin_iface *) oidc_plugin->iface; + if (auth_iface->auth_version != AUTH_PLUGIN_IFACE_VERSION) { + err("auth_oidc plugin interface version mismatch"); + return -1; + } + + as->auth_oidc_iface = auth_iface; + as->auth_oidc_data = oidc_plugin->data; + as->auth_oidc_iface->set_set_command_handler(as->auth_oidc_data, add_handler_wrapper, as); + + if (as->auth_oidc_iface->open(as->auth_oidc_data) < 0) { + err("auth_oidc plugin 'open' failed"); + return -1; + } + + if (as->auth_oidc_iface->check(as->auth_oidc_data) < 0) { + err("auth_oidc plugin 'check' failed"); + return -1; + } + + if (as->auth_oidc_iface->start_thread(as->auth_oidc_data) < 0) { + err("auth_oidc plugin 'start_thread' failed"); + return -1; + } + + return 0; +} + static int load_vcs_gitlab_plugin(struct AppState *as) { @@ -1789,6 +1838,7 @@ load_plugins(struct AppState *as) if (load_auth_google_plugin(as) < 0) return -1; if (load_auth_vk_plugin(as) < 0) return -1; if (load_auth_yandex_plugin(as) < 0) return -1; + if (load_auth_oidc_plugin(as) < 0) return -1; if (load_vcs_gitlab_plugin(as) < 0) return -1; return 0; From d9054a3b49155b9f4391486ea4646799d2692fd9 Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:23:36 +0300 Subject: [PATCH 6/7] implement OIDC plugin support --- lib/oauth.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/oauth.c b/lib/oauth.c index c34fd75e4..0d45c6959 100644 --- a/lib/oauth.c +++ b/lib/oauth.c @@ -1,6 +1,6 @@ /* -*- mode: c; c-basic-offset: 4 -*- */ -/* Copyright (C) 2021-2022 Alexander Chernov */ +/* Copyright (C) 2021-2024 Alexander Chernov */ /* * This program is free software; you can redistribute it and/or modify @@ -31,13 +31,14 @@ struct ProviderInfo int failed; }; -enum { PROVIDER_COUNT = 3 }; +enum { PROVIDER_COUNT = 4 }; static struct ProviderInfo providers[PROVIDER_COUNT] = { { "google" }, { "vk" }, { "yandex" }, + { "oidc" }, }; static oauth_set_command_handler_t oauth_set_command_handler_func = NULL; From 5990258837fc13b0e8adf667cab9accd68ddc9c1 Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Mon, 3 Jun 2024 08:31:44 +0300 Subject: [PATCH 7/7] add OIDC login buttons --- csp/contests/reg_login_page.csp | 9 +++++++++ csp/contests/unpriv_login_page.csp | 9 +++++++++ csp/super-server/login_page.csp | 18 ++++++++++++++++++ style/icons/oidc-logo.svg | 3 +++ 4 files changed, 39 insertions(+) create mode 100644 style/icons/oidc-logo.svg diff --git a/csp/contests/reg_login_page.csp b/csp/contests/reg_login_page.csp index 30c8bd4c2..49f786b47 100644 --- a/csp/contests/reg_login_page.csp +++ b/csp/contests/reg_login_page.csp @@ -118,6 +118,10 @@ + + + +

<% if (oauth_is_available_num(phr->config, 1)) { @@ -133,6 +137,11 @@ if (oauth_is_available_num(phr->config, 3)) { %> icons/yandex-logo.png" alt="yandex auth"> +<% + } + if (oauth_is_available_num(phr->config, 4)) { +%> +icons/oidc-logo.svg" alt="OIDC auth" height="46px"> <% } %> diff --git a/csp/contests/unpriv_login_page.csp b/csp/contests/unpriv_login_page.csp index 3ad0a7fe6..0c00c228c 100644 --- a/csp/contests/unpriv_login_page.csp +++ b/csp/contests/unpriv_login_page.csp @@ -143,6 +143,10 @@ + + + +

<% if (oauth_is_available_num(phr->config, 1)) { @@ -158,6 +162,11 @@ if (oauth_is_available_num(phr->config, 3)) { %> icons/yandex-logo.png" alt="yandex auth"> +<% + } + if (oauth_is_available_num(phr->config, 4)) { +%> +icons/oidc-logo.svg" alt="OIDC auth" height="46px"> <% } %> diff --git a/csp/super-server/login_page.csp b/csp/super-server/login_page.csp index 483f87f2f..07ab405d5 100644 --- a/csp/super-server/login_page.csp +++ b/csp/super-server/login_page.csp @@ -55,6 +55,14 @@ is_configured( + + + + + + + +

<% if (is_configured(phr->config, "google")) { @@ -65,6 +73,16 @@ is_configured( if (is_configured(phr->config, "vk")) { %> icons/vk-logo.jpeg" alt="vk auth" width="46"> +<% + } + if (is_configured(phr->config, "yandex")) { +%> +icons/yandex-logo.png" alt="yandex auth"> +<% + } + if (is_configured(phr->config, "oidc")) { +%> +icons/oidc-logo.svg" alt="OIDC auth" width="46"> <% } %> diff --git a/style/icons/oidc-logo.svg b/style/icons/oidc-logo.svg new file mode 100644 index 000000000..1fad1c6c1 --- /dev/null +++ b/style/icons/oidc-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file