From 5760dbccdb16a546f810f6d0e00aaa560c5183ed Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 17 May 2026 18:21:27 +0300 Subject: [PATCH 1/7] oidc_child: add GSSAPI credential setup from host keytab Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- Makefile.am | 3 + src/oidc_child/oidc_child_gssapi.c | 122 +++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/oidc_child/oidc_child_gssapi.c diff --git a/Makefile.am b/Makefile.am index 19d208b9c34..80a3ce8ad9f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -4917,6 +4917,7 @@ if BUILD_OIDC_CHILD oidc_child_SOURCES = \ src/oidc_child/oidc_child.c \ src/oidc_child/oidc_child_curl.c \ + src/oidc_child/oidc_child_gssapi.c \ src/oidc_child/oidc_child_json.c \ src/oidc_child/oidc_child_id.c \ src/oidc_child/libcrypto/oidc_child_get_jwk.c \ @@ -4937,6 +4938,7 @@ oidc_child_CFLAGS = \ $(CURL_CFLAGS) \ $(UUID_CFLAGS) \ $(CRYPTO_CFLAGS) \ + $(GSSAPI_KRB5_CFLAGS) \ $(NULL) oidc_child_LDADD = \ libsss_debug.la \ @@ -4947,6 +4949,7 @@ oidc_child_LDADD = \ $(CURL_LIBS) \ $(UUID_LIBS) \ $(CRYPTO_LIBS) \ + $(GSSAPI_KRB5_LIBS) \ $(NULL) endif diff --git a/src/oidc_child/oidc_child_gssapi.c b/src/oidc_child/oidc_child_gssapi.c new file mode 100644 index 00000000000..177dc324955 --- /dev/null +++ b/src/oidc_child/oidc_child_gssapi.c @@ -0,0 +1,122 @@ +/* + SSSD + + Helper child for OIDC and OAuth 2.0 Device Authorization Grant + GSSAPI credential setup for Kerberos/SPNEGO client authentication + + Copyright (C) 2025 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 . +*/ + +#include +#include +#include + +#include "util/util.h" +#include "oidc_child/oidc_child_util.h" + +#define OIDC_GSSAPI_CCACHE "MEMORY:oidc_child" + +/* + * Acquire an initiator credential from the host keytab and store it in a + * MEMORY: ccache. Setting KRB5CCNAME to that ccache makes libcurl's + * CURLAUTH_GSSNEGOTIATE pick it up when forming the Authorization: Negotiate + * header for the token / device-auth endpoints. + * + * keytab_name may be NULL to use the system default keytab. + */ +errno_t oidc_setup_gssapi(const char *keytab_name) +{ + OM_uint32 major; + OM_uint32 minor; + gss_cred_id_t cred_handle = GSS_C_NO_CREDENTIAL; + errno_t ret; + + /* Use the system default keytab if none was specified. */ + const char *kt = (keytab_name != NULL) ? keytab_name : "/etc/krb5.keytab"; + + gss_key_value_element_desc acquire_elems[] = { + { "client_keytab", kt } + }; + gss_key_value_set_desc acquire_store = { + .count = 1, + .elements = acquire_elems + }; + + gss_key_value_element_desc store_elems[] = { + { "ccache", OIDC_GSSAPI_CCACHE } + }; + gss_key_value_set_desc store_store = { + .count = 1, + .elements = store_elems + }; + + DEBUG(SSSDBG_TRACE_FUNC, + "Acquiring GSSAPI credentials from keytab [%s].\n", kt); + + major = gss_acquire_cred_from(&minor, + GSS_C_NO_NAME, + GSS_C_INDEFINITE, + GSS_C_NO_OID_SET, + GSS_C_INITIATE, + &acquire_store, + &cred_handle, + NULL, + NULL); + if (GSS_ERROR(major)) { + DEBUG(SSSDBG_CRIT_FAILURE, + "gss_acquire_cred_from failed: major=0x%08x minor=0x%08x\n", + major, minor); + ret = EIO; + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "Storing GSSAPI credentials into ccache [%s].\n", OIDC_GSSAPI_CCACHE); + + major = gss_store_cred_into(&minor, + cred_handle, + GSS_C_INITIATE, + GSS_C_NO_OID, + 1, /* overwrite */ + 1, /* default_cred */ + &store_store, + NULL, + NULL); + if (GSS_ERROR(major)) { + DEBUG(SSSDBG_CRIT_FAILURE, + "gss_store_cred_into failed: major=0x%08x minor=0x%08x\n", + major, minor); + ret = EIO; + goto done; + } + + if (setenv("KRB5CCNAME", OIDC_GSSAPI_CCACHE, 1) != 0) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "setenv(KRB5CCNAME) failed [%d]: %s\n", ret, strerror(ret)); + goto done; + } + + DEBUG(SSSDBG_CONF_SETTINGS, + "GSSAPI credentials ready; KRB5CCNAME=%s\n", OIDC_GSSAPI_CCACHE); + ret = EOK; + +done: + if (cred_handle != GSS_C_NO_CREDENTIAL) { + gss_release_cred(&minor, &cred_handle); + } + return ret; +} From 1f971998a60a13d7cf169189aed46d808c315fc6 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 17 May 2026 18:21:27 +0300 Subject: [PATCH 2/7] oidc_child: add Ahdapa identity lookup Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- src/oidc_child/oidc_child_id.c | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/oidc_child/oidc_child_id.c b/src/oidc_child/oidc_child_id.c index bc06156398f..5cfebac3427 100644 --- a/src/oidc_child/oidc_child_id.c +++ b/src/oidc_child/oidc_child_id.c @@ -474,6 +474,129 @@ errno_t keycloak_lookup(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, return ret; } +/* The following function will lookup users and groups based on Ahdapa's + * REST API at /api/identity/{users,groups}. The JSON schema is compatible + * with keycloak_lookup() – username/name field presence drives type detection + * – but the URL prefix and query parameters differ. */ +errno_t ahdapa_lookup(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, + char *base_url, + char *input, enum search_str_type input_type, + bool libcurl_debug, const char *ca_db, + const char *client_id, const char *client_secret, + const char *token_endpoint, const char *scope, + const char *bearer_token, struct rest_ctx *rest_ctx, + char **out) +{ + errno_t ret; + char *uri; + char *input_enc; + const char *obj_id; + struct name_and_type_identifier ahdapa_map = { + .user_identifier_attr = "username", + .group_identifier_attr = "name", + .user_name_attr = "username", + .group_name_attr = "name" }; + + if (base_url == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Missing base URL in IdP type [ahdapa].\n"); + return EINVAL; + } + + input_enc = url_encode_string(rest_ctx, input); + if (input_enc == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to encode input [%s].\n", input); + return EINVAL; + } + + switch (oidc_cmd) { + case GET_USER: + case GET_USER_GROUPS: + uri = talloc_asprintf(rest_ctx, + "%s/api/identity/users?username=%s&exact=true", + base_url, input_enc); + break; + case GET_GROUP: + case GET_GROUP_MEMBERS: + uri = talloc_asprintf(rest_ctx, + "%s/api/identity/groups?search=%s&exact=true", + base_url, input_enc); + break; + default: + DEBUG(SSSDBG_OP_FAILURE, "Unknown command [%d].\n", oidc_cmd); + return EINVAL; + } + + if (uri == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate lookup URI.\n"); + return ENOMEM; + } + + clean_http_data(rest_ctx); + ret = do_http_request(rest_ctx, uri, NULL, bearer_token); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Search request failed.\n"); + goto done; + } + + if (oidc_cmd == GET_USER || oidc_cmd == GET_GROUP) { + ret = EOK; + goto done; + } + + /* Phase 2: membership / member lookup */ + obj_id = get_str_attr_from_json_array_string(rest_ctx, + get_http_data(rest_ctx), + "id"); + if (obj_id == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to read mandatory object id.\n"); + ret = EINVAL; + goto done; + } + + switch (oidc_cmd) { + case GET_USER_GROUPS: + uri = talloc_asprintf(rest_ctx, + "%s/api/identity/users/%s/groups", + base_url, obj_id); + break; + case GET_GROUP_MEMBERS: + uri = talloc_asprintf(rest_ctx, + "%s/api/identity/groups/%s/members", + base_url, obj_id); + break; + default: + DEBUG(SSSDBG_OP_FAILURE, "Unknown command [%d].\n", oidc_cmd); + ret = EINVAL; + goto done; + } + + if (uri == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate Phase 2 URI.\n"); + ret = ENOMEM; + goto done; + } + + clean_http_data(rest_ctx); + ret = do_http_request_json_data(rest_ctx, uri, NULL, bearer_token); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Member(of) search request failed.\n"); + goto done; + } + + ret = EOK; + +done: + if (ret == EOK && out != NULL) { + ret = add_posix_to_json_string_array(mem_ctx, &ahdapa_map, + 0, get_http_data(rest_ctx), out); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add POSIX data.\n"); + } + } + + return ret; +} + errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, char *idp_type, char *input, enum search_str_type input_type, @@ -535,6 +658,11 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, libcurl_debug, ca_db, client_id, client_secret, token_endpoint, scope, bearer_token, rest_ctx, out); + } else if (idp_type != NULL && strncasecmp(idp_type, "ahdapa:", 7) == 0) { + ret = ahdapa_lookup(mem_ctx, oidc_cmd, base_url, input, input_type, + libcurl_debug, ca_db, client_id, client_secret, + token_endpoint, scope, bearer_token, rest_ctx, + out); } else if (idp_type == NULL || strcasecmp(idp_type, "entra_id") == 0 || strncasecmp(idp_type, "entra_id:", 9) == 0) { From 39e341be3355f7a1cd63bd031c3aaabfe8036c00 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 17 May 2026 18:21:27 +0300 Subject: [PATCH 3/7] oidc_child: support GSSAPI/Negotiate client authentication Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- src/oidc_child/oidc_child.c | 33 ++++++++++++++++++++++++++++++-- src/oidc_child/oidc_child_curl.c | 19 ++++++++++++++++-- src/oidc_child/oidc_child_id.c | 14 ++++++++++---- src/oidc_child/oidc_child_util.h | 8 ++++++-- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/oidc_child/oidc_child.c b/src/oidc_child/oidc_child.c index 15e88d4da69..ee4759c74af 100644 --- a/src/oidc_child/oidc_child.c +++ b/src/oidc_child/oidc_child.c @@ -279,6 +279,7 @@ static errno_t set_endpoints(struct devicecode_ctx *dc_ctx, static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, const char *ca_db, + bool use_gssapi, const char *issuer_url, const char *device_auth_endpoint, const char *token_endpoint, @@ -300,7 +301,7 @@ static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, dc_ctx->rest_ctx = get_rest_ctx(dc_ctx, libcurl_debug, ca_db, pkcs12_client_creds, client_auth_method, - key_passwd); + key_passwd, use_gssapi); if (dc_ctx->rest_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get curl context.\n"); ret = ENOMEM; @@ -350,6 +351,8 @@ struct cli_opts { char *scope; char *client_secret; bool client_secret_stdin; + bool use_gssapi; + char *keytab; char *ca_db; char *user_identifier_attr; bool libcurl_debug; @@ -375,6 +378,7 @@ static void free_cli_opts_members(struct cli_opts *opts) sss_erase_mem_securely(opts->client_secret, strlen(opts->client_secret)); } free(opts->client_secret); + free(opts->keytab); free(opts->ca_db); free(opts->user_identifier_attr); free(opts->search_str); @@ -497,6 +501,10 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) _("Client secret/PKCS#12 password (if needed)"), NULL}, {"client-secret-stdin", 0, POPT_ARG_NONE, NULL, 's', _("Read client secret/PKCS#12 password from standard input"), NULL}, + {"client-use-gssapi", 0, POPT_ARG_NONE, NULL, 'g', + _("Use GSSAPI/Kerberos for client authentication"), NULL}, + {"keytab", 0, POPT_ARG_STRING, &opts->keytab, 0, + _("Keytab file for GSSAPI client authentication"), NULL}, {"idp-type", 0, POPT_ARG_STRING, &opts->idp_type, 0, _("Type of the IdP (entra_id, keycloak etc)"), NULL}, {"name", 0, POPT_ARG_STRING, &tmp_name, 0, _("Name of user or group"), @@ -534,6 +542,9 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) case 's': opts->client_secret_stdin = true; break; + case 'g': + opts->use_gssapi = true; + break; case 'r': opts->return_tokens = true; break; @@ -579,6 +590,13 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) goto done; } + if (opts->use_gssapi && (opts->client_secret != NULL + || opts->client_secret_stdin)) { + fprintf(stderr, "\n--client-use-gssapi and --client-secret* options " + "are mutually exclusive.\n\n"); + goto done; + } + if (tmp_name != NULL) { opts->search_str = tmp_name; opts->search_str_type = TYPE_NAME; @@ -720,6 +738,15 @@ int main(int argc, const char *argv[]) } talloc_steal(main_ctx, debug_prg_name); + if (opts.use_gssapi) { + ret = oidc_setup_gssapi(opts.keytab); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to acquire GSSAPI credentials from keytab.\n"); + goto done; + } + } + if (opts.oidc_cmd == GET_DEVICE_CODE || IS_ID_CMD(opts.oidc_cmd)) { if (opts.client_secret_stdin) { @@ -746,7 +773,8 @@ int main(int argc, const char *argv[]) opts.client_id, opts.client_secret, opts.pkcs12_client_creds, opts.client_auth_method, - opts.token_endpoint, opts.scope, &out); + opts.token_endpoint, opts.scope, + opts.use_gssapi, &out); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Id lookup failed.\n"); goto done; @@ -764,6 +792,7 @@ int main(int argc, const char *argv[]) || opts.oidc_cmd == GET_ACCESS_TOKEN || opts.oidc_cmd == REFRESH_ACCESS_TOKEN) { dc_ctx = get_dc_ctx(main_ctx, opts.libcurl_debug, opts.ca_db, + opts.use_gssapi, opts.issuer_url, opts.device_auth_endpoint, opts.token_endpoint, opts.userinfo_endpoint, opts.jwks_uri, opts.scope, diff --git a/src/oidc_child/oidc_child_curl.c b/src/oidc_child/oidc_child_curl.c index dd17f39d2f6..654855e9abe 100644 --- a/src/oidc_child/oidc_child_curl.c +++ b/src/oidc_child/oidc_child_curl.c @@ -29,6 +29,7 @@ struct rest_ctx { bool libcurl_debug; + bool use_gssapi; const char *ca_db; const char *pkcs12_client_creds; enum client_auth_method client_auth_method; @@ -63,7 +64,7 @@ struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, const char *ca_db, const char *pkcs12_client_creds, enum client_auth_method client_auth_method, - const char *key_passwd) + const char *key_passwd, bool use_gssapi) { struct rest_ctx *rest_ctx; @@ -74,6 +75,7 @@ struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, } rest_ctx->libcurl_debug = libcurl_debug; + rest_ctx->use_gssapi = use_gssapi; if (ca_db != NULL) { rest_ctx->ca_db = talloc_strdup(rest_ctx, ca_db); if (rest_ctx->ca_db == NULL) { @@ -363,7 +365,20 @@ static errno_t set_http_opts(CURL *curl_ctx, struct rest_ctx *rest_ctx, } } - if (token != NULL) { + if (rest_ctx->use_gssapi) { + res = curl_easy_setopt(curl_ctx, CURLOPT_HTTPAUTH, CURLAUTH_GSSNEGOTIATE); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set GSSAPI/Negotiate auth.\n"); + ret = EIO; + goto done; + } + res = curl_easy_setopt(curl_ctx, CURLOPT_USERPWD, ":"); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set empty credentials for GSSAPI.\n"); + ret = EIO; + goto done; + } + } else 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"); diff --git a/src/oidc_child/oidc_child_id.c b/src/oidc_child/oidc_child_id.c index 5cfebac3427..84d4ba65159 100644 --- a/src/oidc_child/oidc_child_id.c +++ b/src/oidc_child/oidc_child_id.c @@ -604,7 +604,8 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, const char *client_id, const char *client_secret, const char *pkcs12_client_creds, enum client_auth_method client_auth_method, - const char *token_endpoint, const char *scope, char **out) + const char *token_endpoint, const char *scope, + bool use_gssapi, char **out) { errno_t ret; struct rest_ctx *rest_ctx; @@ -616,15 +617,20 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, return EINVAL; } - if (client_id == NULL || client_secret == NULL || token_endpoint == NULL - || input == NULL) { + if (client_id == NULL || token_endpoint == NULL || input == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing required argument.\n"); return EINVAL; } + if (!use_gssapi && client_secret == NULL + && client_auth_method == CAM_SECRET) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing client_secret (required without GSSAPI).\n"); + return EINVAL; + } + rest_ctx = get_rest_ctx(mem_ctx, libcurl_debug, ca_db, pkcs12_client_creds, client_auth_method, - client_secret); + client_secret, use_gssapi); if (rest_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get REST context.\n"); return ENOMEM; diff --git a/src/oidc_child/oidc_child_util.h b/src/oidc_child/oidc_child_util.h index d11fb2cb1d8..1beeba2701a 100644 --- a/src/oidc_child/oidc_child_util.h +++ b/src/oidc_child/oidc_child_util.h @@ -100,7 +100,7 @@ struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, const char *ca_db, const char *pkcs12_client_creds, enum client_auth_method client_auth_method, - const char *key_passwd); + const char *key_passwd, bool use_gssapi); const char *get_http_data(struct rest_ctx *rest_ctx); @@ -200,6 +200,9 @@ json_t *token_data_to_json(struct devicecode_ctx *dc_ctx); char *get_jwt(struct rest_ctx *rest_ctx, const char *token_endpoint, const char *client_id); +/* oidc_child_gssapi.c */ +errno_t oidc_setup_gssapi(const char *keytab_name); + /* oidc_child_id.c */ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, char *idp_type, @@ -208,7 +211,8 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, const char *client_id, const char *client_secret, const char *pkcs12_client_creds, enum client_auth_method client_auth_method, - const char *token_endpoint, const char *scope, char **out); + const char *token_endpoint, const char *scope, + bool use_gssapi, char **out); /* libcrypto/oidc_child_get_jwk.c */ errno_t get_jwk_from_pkcs12(TALLOC_CTX *mem_ctx, From 00e6b94e4f5b1872f6b57257ece775a606cfc407 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 17 May 2026 18:21:27 +0300 Subject: [PATCH 4/7] idp: add idp_client_use_gssapi option for Kerberos client auth Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- src/providers/idp/idp_auth.c | 28 +++++++++++++++++--------- src/providers/idp/idp_auth.h | 2 ++ src/providers/idp/idp_common.h | 6 +++++- src/providers/idp/idp_id.c | 4 +++- src/providers/idp/idp_id.h | 2 ++ src/providers/idp/idp_init.c | 11 +++++++++- src/providers/idp/idp_opts.c | 2 ++ src/providers/idp/oidc_child_handler.c | 24 ++++++++++++++++++++-- 8 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/providers/idp/idp_auth.c b/src/providers/idp/idp_auth.c index 4159df0b3f6..62e140eeb1a 100644 --- a/src/providers/idp/idp_auth.c +++ b/src/providers/idp/idp_auth.c @@ -88,7 +88,9 @@ set_oidc_auth_extra_args(TALLOC_CTX *mem_ctx, struct idp_auth_ctx *idp_auth_ctx, idp_auth_ctx->client_id, idp_auth_ctx->client_secret, idp_auth_ctx->token_endpoint, - idp_auth_ctx->scope); + idp_auth_ctx->scope, + idp_auth_ctx->use_gssapi, + idp_auth_ctx->keytab); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to set common arguments.\n"); goto done; @@ -191,10 +193,14 @@ static const char *get_stored_request_data(TALLOC_CTX *mem_ctx, goto done; } - send_data = talloc_asprintf(mem_ctx, "%s\n%s", - dp_opt_get_cstring(idp_auth_ctx->idp_options, - IDP_CLIENT_SECRET), - open_req->device_code_data); + if (idp_auth_ctx->use_gssapi) { + send_data = talloc_strdup(mem_ctx, open_req->device_code_data); + } else { + send_data = talloc_asprintf(mem_ctx, "%s\n%s", + dp_opt_get_cstring(idp_auth_ctx->idp_options, + IDP_CLIENT_SECRET), + open_req->device_code_data); + } if (send_data == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to generate auth data.\n"); goto done; @@ -244,10 +250,14 @@ static const char *get_refresh_request_data(TALLOC_CTX *mem_ctx, goto done; } - send_data = talloc_asprintf(mem_ctx, "%s\n%s", - dp_opt_get_cstring(idp_auth_ctx->idp_options, - IDP_CLIENT_SECRET), - token); + if (idp_auth_ctx->use_gssapi) { + send_data = talloc_strdup(mem_ctx, token); + } else { + send_data = talloc_asprintf(mem_ctx, "%s\n%s", + dp_opt_get_cstring(idp_auth_ctx->idp_options, + IDP_CLIENT_SECRET), + token); + } if (send_data == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to generate token refresh data.\n"); goto done; diff --git a/src/providers/idp/idp_auth.h b/src/providers/idp/idp_auth.h index 985670c6722..3d1656037c2 100644 --- a/src/providers/idp/idp_auth.h +++ b/src/providers/idp/idp_auth.h @@ -42,6 +42,8 @@ struct idp_auth_ctx { const char *device_auth_endpoint; const char *userinfo_endpoint; const char *scope; + bool use_gssapi; + const char *keytab; hash_table_t *token_refresh_table; }; diff --git a/src/providers/idp/idp_common.h b/src/providers/idp/idp_common.h index f274036238e..43a51f2d5d6 100644 --- a/src/providers/idp/idp_common.h +++ b/src/providers/idp/idp_common.h @@ -43,6 +43,8 @@ enum idp_opts { IDP_ID_SCOPE, IDP_AUTH_SCOPE, IDP_AUTO_REFRESH, + IDP_CLIENT_USE_GSSAPI, + IDP_CLIENT_KEYTAB, IDMAP_LOWER, IDMAP_UPPER, IDMAP_RANGESIZE, @@ -83,5 +85,7 @@ errno_t set_oidc_common_args(const char **extra_args, size_t *c, const char *client_id, const char *client_secret, const char *token_endpoint, - const char *scope); + const char *scope, + bool use_gssapi, + const char *keytab); #endif /* __IDP_COMMON_H__ */ diff --git a/src/providers/idp/idp_id.c b/src/providers/idp/idp_id.c index 8ad85fb9217..62efcebebf4 100644 --- a/src/providers/idp/idp_id.c +++ b/src/providers/idp/idp_id.c @@ -100,7 +100,9 @@ errno_t set_oidc_extra_args(TALLOC_CTX *mem_ctx, struct idp_id_ctx *idp_id_ctx, idp_id_ctx->client_id, idp_id_ctx->client_secret, idp_id_ctx->token_endpoint, - idp_id_ctx->scope); + idp_id_ctx->scope, + idp_id_ctx->use_gssapi, + idp_id_ctx->keytab); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to add common arguments.\n"); goto done; diff --git a/src/providers/idp/idp_id.h b/src/providers/idp/idp_id.h index 10470f2d962..c41691bc353 100644 --- a/src/providers/idp/idp_id.h +++ b/src/providers/idp/idp_id.h @@ -40,6 +40,8 @@ struct idp_id_ctx { const char *client_secret; const char *token_endpoint; const char *scope; + bool use_gssapi; + const char *keytab; }; struct tevent_req * diff --git a/src/providers/idp/idp_init.c b/src/providers/idp/idp_init.c index 1df626d7b1d..a896bde88ec 100644 --- a/src/providers/idp/idp_init.c +++ b/src/providers/idp/idp_init.c @@ -42,6 +42,8 @@ struct idp_init_ctx { const char *client_secret; const char *token_endpoint; const char *scope; + bool use_gssapi; + const char *keytab; }; static void token_refresh_table_delete_cb(hash_entry_t *item, @@ -122,9 +124,12 @@ errno_t sssm_idp_init(TALLOC_CTX *mem_ctx, goto done; } + init_ctx->use_gssapi = dp_opt_get_bool(init_ctx->opts, IDP_CLIENT_USE_GSSAPI); + init_ctx->keytab = dp_opt_get_cstring(init_ctx->opts, IDP_CLIENT_KEYTAB); + init_ctx->client_secret = dp_opt_get_cstring(init_ctx->opts, IDP_CLIENT_SECRET); - if (init_ctx->client_secret == NULL) { + if (init_ctx->client_secret == NULL && !init_ctx->use_gssapi) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing required option '"CONFDB_IDP_CLIENT_SECRET"'.\n"); ret = EINVAL; @@ -256,6 +261,8 @@ errno_t sssm_idp_id_init(TALLOC_CTX *mem_ctx, id_ctx->client_secret = init_ctx->client_secret; id_ctx->token_endpoint = init_ctx->token_endpoint; id_ctx->scope = init_ctx->scope; + id_ctx->use_gssapi = init_ctx->use_gssapi; + id_ctx->keytab = init_ctx->keytab; err = sss_idmap_init(sss_idmap_talloc, init_ctx, sss_idmap_talloc_free, &id_ctx->idmap_ctx); @@ -346,6 +353,8 @@ errno_t sssm_idp_auth_init(TALLOC_CTX *mem_ctx, auth_ctx->client_id = init_ctx->client_id; auth_ctx->client_secret = init_ctx->client_secret; auth_ctx->token_endpoint = init_ctx->token_endpoint; + auth_ctx->use_gssapi = init_ctx->use_gssapi; + auth_ctx->keytab = init_ctx->keytab; auth_ctx->open_request_table = sss_ptr_hash_create(auth_ctx, NULL, NULL); if (auth_ctx->open_request_table == NULL) { diff --git a/src/providers/idp/idp_opts.c b/src/providers/idp/idp_opts.c index f81dd6581ee..2395eaf802f 100644 --- a/src/providers/idp/idp_opts.c +++ b/src/providers/idp/idp_opts.c @@ -35,6 +35,8 @@ struct dp_option default_idp_opts[] = { { "idp_id_scope", DP_OPT_STRING, NULL_STRING, NULL_STRING }, { "idp_auth_scope", DP_OPT_STRING, NULL_STRING, NULL_STRING }, { "idp_auto_refresh", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "idp_client_use_gssapi", DP_OPT_BOOL, BOOL_FALSE, BOOL_FALSE }, + { "idp_client_keytab", DP_OPT_STRING, NULL_STRING, NULL_STRING }, { "idmap_range_min", DP_OPT_NUMBER, { .number = 200000 }, NULL_NUMBER }, { "idmap_range_max", DP_OPT_NUMBER, { .number = 2000200000LL }, NULL_NUMBER }, { "idmap_range_size", DP_OPT_NUMBER, { .number = 200000 }, NULL_NUMBER }, diff --git a/src/providers/idp/oidc_child_handler.c b/src/providers/idp/oidc_child_handler.c index ad3e3f6b1e2..346dc7cee49 100644 --- a/src/providers/idp/oidc_child_handler.c +++ b/src/providers/idp/oidc_child_handler.c @@ -229,7 +229,9 @@ errno_t set_oidc_common_args(const char **extra_args, size_t *c, const char *client_id, const char *client_secret, const char *token_endpoint, - const char *scope) + const char *scope, + bool use_gssapi, + const char *keytab) { int ret; @@ -253,7 +255,25 @@ errno_t set_oidc_common_args(const char **extra_args, size_t *c, } (*c)++; - if (client_secret != NULL) { + if (use_gssapi) { + extra_args[*c] = talloc_strdup(extra_args, "--client-use-gssapi"); + if (extra_args[*c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + ret = ENOMEM; + goto done; + } + (*c)++; + + if (keytab != NULL) { + extra_args[*c] = talloc_asprintf(extra_args, "--keytab=%s", keytab); + if (extra_args[*c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); + ret = ENOMEM; + goto done; + } + (*c)++; + } + } else if (client_secret != NULL) { extra_args[*c] = talloc_strdup(extra_args, "--client-secret-stdin"); if (extra_args[*c] == NULL) { From 142b29100e0dc9f3d6f034e061313f3c98887601 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 17 May 2026 18:21:27 +0300 Subject: [PATCH 5/7] idp: auto-derive Ahdapa endpoints from idp_type base URL Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- src/providers/idp/idp_init.c | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/providers/idp/idp_init.c b/src/providers/idp/idp_init.c index a896bde88ec..cbaeabe7ab5 100644 --- a/src/providers/idp/idp_init.c +++ b/src/providers/idp/idp_init.c @@ -22,6 +22,8 @@ along with this program. If not, see . */ +#include + #include "src/providers/data_provider.h" #include "src/providers/idp/idp_common.h" @@ -31,6 +33,37 @@ #include "lib/idmap/sss_idmap.h" #include "util/util_sss_idmap.h" +/* If idp_type is "ahdapa:", return a talloc'd copy of with + * trailing slashes stripped. Returns NULL for any other idp_type. */ +static char *ahdapa_base_url(TALLOC_CTX *mem_ctx, const char *idp_type) +{ + const char *base; + char *url; + char *p; + + if (idp_type == NULL + || strncasecmp(idp_type, "ahdapa:", 7) != 0) { + return NULL; + } + + base = idp_type + 7; + if (*base == '\0') { + return NULL; + } + + url = talloc_strdup(mem_ctx, base); + if (url == NULL) { + return NULL; + } + + p = url + strlen(url) - 1; + while (p > url && *p == '/') { + *p-- = '\0'; + } + + return url; +} + struct idp_init_ctx { struct be_ctx *be_ctx; struct dp_option *opts; @@ -138,6 +171,14 @@ errno_t sssm_idp_init(TALLOC_CTX *mem_ctx, init_ctx->token_endpoint = dp_opt_get_cstring(init_ctx->opts, IDP_TOKEN_ENDPOINT); + if (init_ctx->token_endpoint == NULL) { + char *base = ahdapa_base_url(init_ctx, init_ctx->idp_type); + if (base != NULL) { + init_ctx->token_endpoint = talloc_asprintf(init_ctx, + "%s/token", base); + talloc_free(base); + } + } if (init_ctx->token_endpoint == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing required option 'idp_token_endpoint'.\n"); @@ -386,6 +427,14 @@ errno_t sssm_idp_auth_init(TALLOC_CTX *mem_ctx, auth_ctx->device_auth_endpoint = dp_opt_get_cstring(init_ctx->opts, IDP_DEVICE_AUTH_ENDPOINT); + if (auth_ctx->device_auth_endpoint == NULL) { + char *base = ahdapa_base_url(auth_ctx, init_ctx->idp_type); + if (base != NULL) { + auth_ctx->device_auth_endpoint = talloc_asprintf(auth_ctx, + "%s/device_authorization", base); + talloc_free(base); + } + } if (auth_ctx->device_auth_endpoint == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing required option 'idp_device_code_endpoint'.\n"); @@ -395,6 +444,14 @@ errno_t sssm_idp_auth_init(TALLOC_CTX *mem_ctx, auth_ctx->userinfo_endpoint = dp_opt_get_cstring(init_ctx->opts, IDP_USERINFO_ENDPOINT); + if (auth_ctx->userinfo_endpoint == NULL) { + char *base = ahdapa_base_url(auth_ctx, init_ctx->idp_type); + if (base != NULL) { + auth_ctx->userinfo_endpoint = talloc_asprintf(auth_ctx, + "%s/userinfo", base); + talloc_free(base); + } + } if (auth_ctx->userinfo_endpoint == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing required option 'idp_userinfo_endpoint'.\n"); From 00fd3ee09968cca65ea8f4aa18133fa9bdd59123 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Mon, 18 May 2026 08:31:18 +0300 Subject: [PATCH 6/7] man: update sssd-idp.5 for GSSAPI client auth and Ahdapa support Document the two new options added by 'idp: add idp_client_use_gssapi option for Kerberos client auth': - idp_client_use_gssapi (boolean, default false): use host Kerberos keytab instead of a client secret at the token/device-auth endpoints - idp_client_keytab (string, default /etc/krb5.keytab): keytab path for GSSAPI credential acquisition Update idp_client_secret to note it is optional when use_gssapi = true. Document the FreeIPA integrated IdP (Ahdapa) as a supported idp_type value. Describe the 'Ahdapa:' format introduced by 'idp: auto-derive Ahdapa endpoints from idp_type base URL': the token, device authorization, and userinfo endpoints are automatically derived from the base URL. Note that in a standard FreeIPA deployment Ahdapa is mounted at the /idp path, so the typical value is 'Ahdapa:https://server.example.com/idp'. Update idp_token_endpoint, idp_device_auth_endpoint, and idp_userinfo_endpoint to reflect that they are auto-derived for the Ahdapa type. Add an Ahdapa GSSAPI example to the EXAMPLE section. Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- src/man/sssd-idp.5.xml | 165 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 145 insertions(+), 20 deletions(-) diff --git a/src/man/sssd-idp.5.xml b/src/man/sssd-idp.5.xml index 6166fd2fb50..82fdbf83be0 100644 --- a/src/man/sssd-idp.5.xml +++ b/src/man/sssd-idp.5.xml @@ -63,20 +63,51 @@ Required option that specifies the IdP product. - Currently Entra ID (entra_id) and Keycloak - (keycloak) are supported. + Currently Entra ID (entra_id), + Keycloak (keycloak), and the + FreeIPA integrated IdP (ahdapa) + are supported. - Depending on the IdP product additional platform - specific options might follow the name separated - by a colon (:). E.g. for Keycloak the base URI for - the user and group REST API must be given. For - Entra ID the base URI for the Microsoft Graph API - can be given to use sovereign or government cloud - endpoints instead of the default - (https://graph.microsoft.com/v1.0). E.g. - entra_id:https://graph.microsoft.us/v1.0 - for the US government cloud (GCC High). + Depending on the IdP product, additional + platform-specific options may follow the name + separated by a colon (:). + + + For Keycloak, the + base URI for the user and group REST API must be + given, e.g. + keycloak:https://master.keycloak.test:8443/auth/admin/realms/master/. + + + For Entra ID, the + base URI for the Microsoft Graph API can be given to + use sovereign or government cloud endpoints instead + of the default + (https://graph.microsoft.com/v1.0), + e.g. + entra_id:https://graph.microsoft.us/v1.0 + for the US government cloud (GCC High). + + + For the FreeIPA integrated + IdP (ahdapa), the base URL of the + ahdapa server must be given. SSSD will + automatically derive the token endpoint + (<base>/token), the device + authorization endpoint + (<base>/device_authorization), + and the userinfo endpoint + (<base>/userinfo) from this + base URL when the corresponding explicit options are + not set. Explicit values for those options take + precedence if provided. + + + In a standard FreeIPA deployment ahdapa is mounted + at the /idp path of the IPA + server, so the value would be + ahdapa:https://server.example.com/idp. Default: Not set (Required) @@ -103,10 +134,13 @@ idp_client_secret (string) - Password of the IdP client. The password is - required for the id_provider. If only used as + Password of the IdP client. Required for the + id_provider unless + idp_client_use_gssapi = true is set, + in which case GSSAPI/Kerberos is used instead and + no client secret is needed. If only used as auth_provider it depends on the server side - configuration if it is required or not. + configuration whether it is required or not. Default: Not set @@ -118,9 +152,13 @@ IdP endpoint for requesting access tokens. + When idp_type is set to + ahdapa:<base> and this + option is not set, it defaults to + <base>/token. - Default: Not set (Required) + Default: Not set (Required unless auto-derived) @@ -130,10 +168,13 @@ IdP endpoint for device authorization according to RFC-8628. This is required for user - authentication. + authentication. When idp_type is + set to ahdapa:<base> and + this option is not set, it defaults to + <base>/device_authorization. - Default: Not set + Default: Not set (Required unless auto-derived) @@ -143,10 +184,14 @@ IdP userinfo endpoint to request user attributes after a successful authentication of the user. - Required for authentication. + Required for authentication. When + idp_type is set to + ahdapa:<base> and this + option is not set, it defaults to + <base>/userinfo. - Default: Not set + Default: Not set (Required unless auto-derived) @@ -217,6 +262,68 @@ + + idp_client_use_gssapi (boolean) + + + When set to true, SSSD authenticates + to the IdP token and device authorization endpoints + using GSSAPI/Kerberos (HTTP Negotiate / + SPNEGO) instead of a static client secret. + The host Kerberos keytab is used to acquire + credentials for the machine's + host/hostname@REALM principal, + which are stored in a per-invocation + MEMORY: credential cache. + libcurl then presents a Negotiate token on the + token endpoint; the server verifies the AP-REQ and + issues an access token. + + + This option is designed for FreeIPA-enrolled + machines where a single template OAuth2 client is + registered on the IdP with + token_endpoint_auth_method = kerberos_client_auth + and a principal glob such as + host/*@REALM. No per-machine + idp_client_secret needs to be + distributed or rotated. + + + When idp_client_use_gssapi = true, + idp_client_secret must not be set. + GSSAPI is used only at the token and device + authorization endpoints; all identity API calls use + a Bearer token obtained from the token endpoint. + + + Default: false + + + + + idp_client_keytab (string) + + + Path to the Kerberos keytab used to acquire + credentials when + idp_client_use_gssapi = true. + The keytab must contain the host principal + (host/hostname@REALM) and must + be readable by the sssd process. + + + Because /etc/krb5.keytab is + typically readable only by root, a keytab copy with + appropriate permissions should be created and + referenced here (similar to the approach used for + IPA trust keytabs). + + + Default: /etc/krb5.keytab + + + idmap_range_min (integer) @@ -294,6 +401,24 @@ idp_userinfo_endpoint = https://master.keycloak.test:8443/auth/realms/master/pro idp_device_auth_endpoint = https://master.keycloak.test:8443/auth/realms/master/protocol/openid-connect/auth/device idp_id_scope = profile idp_auth_scope = openid profile email + + +[domain/ahdapa] +id_provider = idp +auth_provider = idp + +; idp_type sets the base URL; token, device-auth, and userinfo endpoints +; are derived automatically as <base>/token, /device_authorization, /userinfo +idp_type = ahdapa:https://idp.example.com + +idp_client_id = <client_id of the template kerberos_client_auth client> +; Use the host Kerberos keytab instead of a static client secret. +; The keytab must be readable by the sssd process. +idp_client_use_gssapi = true +idp_client_keytab = /etc/sssd/host.keytab + +idp_id_scope = openid directory.read +idp_auth_scope = openid From e48c0ffffb1933347595300ccc720efcf92aedd6 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Mon, 22 Jun 2026 09:07:05 +0300 Subject: [PATCH 7/7] spec: serialize make install to avoid libtool relink races Libtool relinks sssdlib modules against pkglib libraries during install. With parallel install, the relink can run before the target libraries are in place, causing missing .so files in the buildroot. Use -j1 for the install phase to guarantee correct ordering. Signed-off-by: Alexander Bokovoy Assisted-By: Claude Code (claude-opus-4-6) --- contrib/sssd.spec.in | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contrib/sssd.spec.in b/contrib/sssd.spec.in index 51feecd2d77..b5496c49f19 100644 --- a/contrib/sssd.spec.in +++ b/contrib/sssd.spec.in @@ -539,7 +539,10 @@ unset CK_TIMEOUT_MULTIPLIER %install -%make_install +# Serial install: libtool relinks sssdlib modules against pkglib +# libraries during install. Parallel install can relink before the +# target libraries are in place, causing missing .so files. +%make_install -j1 # Prepare language files /usr/lib/rpm/find-lang.sh $RPM_BUILD_ROOT sssd