Coverage Report

Created: 2026-08-31 07:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mod_auth_openidc/src/metadata/client.c
Line
Count
Source
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one
3
 * or more contributor license agreements.  See the NOTICE file
4
 * distributed with this work for additional information
5
 * regarding copyright ownership.  The ASF licenses this file
6
 * to you under the Apache License, Version 2.0 (the
7
 * "License"); you may not use this file except in compliance
8
 * with the License.  You may obtain a copy of the License at
9
 *
10
 *   http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing,
13
 * software distributed under the License is distributed on an
14
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
 * KIND, either express or implied.  See the License for the
16
 * specific language governing permissions and limitations
17
 * under the License.
18
 */
19
20
/***************************************************************************
21
 * Copyright (C) 2017-2026 ZmartZone Holding BV
22
 * Copyright (C) 2013-2017 Ping Identity Corporation
23
 * All rights reserved.
24
 *
25
 * Client metadata: validation, parsing, on-disk read, and Dynamic Client
26
 * Registration with the OP.
27
 *
28
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
29
 */
30
31
#include "metadata/internal.h"
32
#include "util/util_cfg.h"
33
34
#include "cfg/dir.h"
35
#include "http.h"
36
#include "metrics.h"
37
#include "mod_auth_openidc.h"
38
#include "proto/proto.h"
39
#include "util/util.h"
40
41
#include <apr_strings.h>
42
43
/*
44
 * check to see if dynamically registered JSON client metadata is valid and has not expired
45
 */
46
0
static apr_byte_t oidc_metadata_client_is_valid(request_rec *r, const oidc_json_t *j_client, const char *issuer) {
47
48
0
  char *str;
49
50
  /* get a handle to the client_id we need to use for this provider */
51
0
  str = NULL;
52
0
  oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_ID, &str, NULL);
53
0
  if (str == NULL) {
54
0
    oidc_error(r, "client (%s) JSON metadata did not contain a \"" OIDC_METADATA_CLIENT_ID "\" string",
55
0
         issuer);
56
0
    return FALSE;
57
0
  }
58
59
  /* get a handle to the client_secret we need to use for this provider */
60
0
  str = NULL;
61
0
  oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_SECRET, &str, NULL);
62
0
  if (str == NULL) {
63
0
    oidc_warn(r, "client (%s) JSON metadata did not contain a \"" OIDC_METADATA_CLIENT_SECRET "\" string",
64
0
        issuer);
65
0
  }
66
67
  /* the expiry timestamp from the JSON object */
68
0
  const oidc_json_t *expires_at = oidc_json_object_get(j_client, OIDC_METADATA_CLIENT_SECRET_EXPIRES_AT);
69
0
  if ((expires_at == NULL) || (!oidc_json_is_integer(expires_at))) {
70
0
    oidc_debug(
71
0
        r, "client (%s) metadata did not contain a \"" OIDC_METADATA_CLIENT_SECRET_EXPIRES_AT "\" setting",
72
0
        issuer);
73
    /* assume that it never expires */
74
0
    return TRUE;
75
0
  }
76
77
  /* see if it is unrestricted */
78
0
  if (oidc_json_integer_value(expires_at) == 0) {
79
0
    oidc_debug(r, "client (%s) metadata never expires (" OIDC_METADATA_CLIENT_SECRET_EXPIRES_AT "=0)",
80
0
         issuer);
81
0
    return TRUE;
82
0
  }
83
84
  /* check if the value >= now */
85
0
  if (apr_time_sec(apr_time_now()) > oidc_json_integer_value(expires_at)) {
86
0
    oidc_warn(r, "client (%s) secret expired", issuer);
87
0
    return FALSE;
88
0
  }
89
90
0
  oidc_debug(r, "client (%s) metadata is valid", issuer);
91
92
0
  return TRUE;
93
0
}
94
95
/*
96
 * register the client with the OP using Dynamic Client Registration
97
 */
98
apr_byte_t oidc_metadata_client_register(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
99
0
           oidc_json_t **j_client, char **response) {
100
101
  /* assemble the JSON registration request */
102
0
  oidc_json_t *data = oidc_json_object();
103
0
  oidc_json_object_set_new(data, OIDC_METADATA_CLIENT_NAME,
104
0
         oidc_json_string(oidc_cfg_provider_client_name_get(provider)));
105
106
0
  oidc_json_t *redirect_uris = oidc_json_array();
107
0
  oidc_json_array_append_new(redirect_uris, oidc_json_string(oidc_util_url_redirect_uri(r, cfg)));
108
0
  oidc_json_object_set_new(data, OIDC_METADATA_REDIRECT_URIS, redirect_uris);
109
110
0
  oidc_json_t *response_types = oidc_json_array();
111
0
  apr_array_header_t *flows = oidc_proto_supported_flows(r->pool);
112
0
  for (int i = 0; i < flows->nelts; i++)
113
0
    oidc_json_array_append_new(response_types, oidc_json_string(APR_ARRAY_IDX(flows, i, const char *)));
114
0
  oidc_json_object_set_new(data, OIDC_METADATA_RESPONSE_TYPES, response_types);
115
116
0
  oidc_json_t *grant_types = oidc_json_array();
117
0
  oidc_json_array_append_new(grant_types, oidc_json_string("authorization_code"));
118
0
  oidc_json_array_append_new(grant_types, oidc_json_string("implicit"));
119
0
  oidc_json_array_append_new(grant_types, oidc_json_string("refresh_token"));
120
0
  oidc_json_object_set_new(data, OIDC_METADATA_GRANT_TYPES, grant_types);
121
122
0
  if (oidc_cfg_provider_token_endpoint_auth_get(provider) != NULL)
123
0
    oidc_json_object_set_new(data, OIDC_METADATA_TOKEN_ENDPOINT_AUTH_METHOD,
124
0
           oidc_json_string(oidc_cfg_provider_token_endpoint_auth_get(provider)));
125
126
  /* Request certificate-bound tokens when the provider's resolved RFC 8705 mode requires them. */
127
0
  if ((oidc_cfg_provider_cert_bound_tokens_get(provider) == OIDC_CERT_BOUND_TOKENS_ON) ||
128
0
      (oidc_cfg_endpoint_auth_is_mtls(oidc_cfg_provider_token_endpoint_auth_get(provider)) == TRUE))
129
0
    oidc_json_object_set_new(data, OIDC_METADATA_TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS,
130
0
           oidc_json_boolean(1));
131
132
0
  if (oidc_cfg_provider_client_contact_get(provider) != NULL) {
133
0
    oidc_json_t *contacts = oidc_json_array();
134
0
    oidc_json_array_append_new(contacts, oidc_json_string(oidc_cfg_provider_client_contact_get(provider)));
135
0
    oidc_json_object_set_new(data, OIDC_METADATA_CONTACTS, contacts);
136
0
  }
137
138
0
  if (oidc_cfg_provider_client_jwks_uri_get(provider)) {
139
0
    oidc_json_object_set_new(data, OIDC_METADATA_JWKS_URI,
140
0
           oidc_json_string(oidc_cfg_provider_client_jwks_uri_get(provider)));
141
0
  } else if (oidc_cfg_public_keys_get(cfg) != NULL) {
142
0
    oidc_json_object_set_new(
143
0
        data, OIDC_METADATA_JWKS_URI,
144
0
        oidc_json_string(apr_psprintf(r->pool, "%s?%s=rsa", oidc_util_url_redirect_uri(r, cfg),
145
0
              OIDC_REDIRECT_URI_REQUEST_JWKS)));
146
0
  }
147
148
0
  if (oidc_cfg_provider_id_token_signed_response_alg_get(provider) != NULL) {
149
0
    oidc_json_object_set_new(
150
0
        data, OIDC_METADATA_ID_TOKEN_SIGNED_RESPONSE_ALG,
151
0
        oidc_json_string(oidc_cfg_provider_id_token_signed_response_alg_get(provider)));
152
0
  }
153
0
  if (oidc_cfg_provider_id_token_encrypted_response_alg_get(provider) != NULL) {
154
0
    oidc_json_object_set_new(
155
0
        data, OIDC_METADATA_ID_TOKEN_ENCRYPTED_RESPONSE_ALG,
156
0
        oidc_json_string(oidc_cfg_provider_id_token_encrypted_response_alg_get(provider)));
157
0
  }
158
0
  if (oidc_cfg_provider_id_token_encrypted_response_enc_get(provider) != NULL) {
159
0
    oidc_json_object_set_new(
160
0
        data, OIDC_METADATA_ID_TOKEN_ENCRYPTED_RESPONSE_ENC,
161
0
        oidc_json_string(oidc_cfg_provider_id_token_encrypted_response_enc_get(provider)));
162
0
  }
163
164
0
  if (oidc_cfg_provider_userinfo_signed_response_alg_get(provider) != NULL) {
165
0
    oidc_json_object_set_new(
166
0
        data, OIDC_METADATA_USERINFO_SIGNED_RESPONSE_ALG,
167
0
        oidc_json_string(oidc_cfg_provider_userinfo_signed_response_alg_get(provider)));
168
0
  }
169
0
  if (oidc_cfg_provider_userinfo_encrypted_response_alg_get(provider) != NULL) {
170
0
    oidc_json_object_set_new(
171
0
        data, OIDC_METADATA_USERINFO_ENCRYPTED_RESPONSE_ALG,
172
0
        oidc_json_string(oidc_cfg_provider_userinfo_encrypted_response_alg_get(provider)));
173
0
  }
174
0
  if (oidc_cfg_provider_userinfo_encrypted_response_enc_get(provider) != NULL) {
175
0
    oidc_json_object_set_new(
176
0
        data, OIDC_METADATA_USERINFO_ENCRYPTED_RESPONSE_ENC,
177
0
        oidc_json_string(oidc_cfg_provider_userinfo_encrypted_response_enc_get(provider)));
178
0
  }
179
180
0
  if (oidc_cfg_provider_request_object_get(provider) != NULL) {
181
0
    oidc_json_t *request_object_config = NULL;
182
0
    if (oidc_json_decode_object(r, oidc_cfg_provider_request_object_get(provider),
183
0
              &request_object_config) == TRUE) {
184
0
      const oidc_json_t *crypto =
185
0
          oidc_json_object_get(request_object_config, OIDC_REQUEST_OBJECT_CRYPTO);
186
0
      char *alg = "none";
187
0
      oidc_json_object_get_string(r->pool, crypto, OIDC_REQUEST_OBJECT_CRYPTO_SIGN_ALG, &alg, "none");
188
0
      oidc_json_object_set_new(data, OIDC_METADATA_REQUEST_OBJECT_SIGNING_ALG, oidc_json_string(alg));
189
0
      oidc_json_decref(request_object_config);
190
0
    }
191
0
  }
192
193
0
  oidc_json_object_set_new(data, OIDC_METADATA_INITIATE_LOGIN_URI,
194
0
         oidc_json_string(oidc_util_url_redirect_uri(r, cfg)));
195
196
0
  oidc_json_object_set_new(
197
0
      data, OIDC_METADATA_FRONTCHANNEL_LOGOUT_URI,
198
0
      oidc_json_string(apr_psprintf(r->pool, "%s?%s=%s", oidc_util_url_redirect_uri(r, cfg),
199
0
            OIDC_REDIRECT_URI_REQUEST_LOGOUT, OIDC_GET_STYLE_LOGOUT_PARAM_VALUE)));
200
201
  // NB: "backchannel_logout_session_required" is not advertised in the registration
202
0
  oidc_json_object_set_new(data, OIDC_METADATA_BACKCHANNEL_LOGOUT_URI,
203
0
         oidc_json_string(apr_psprintf(r->pool, "%s?%s=%s", oidc_util_url_redirect_uri(r, cfg),
204
0
                     OIDC_REDIRECT_URI_REQUEST_LOGOUT,
205
0
                     OIDC_BACKCHANNEL_STYLE_LOGOUT_PARAM_VALUE)));
206
207
0
  if (oidc_cfg_default_slo_url_get(cfg) != NULL) {
208
0
    oidc_json_t *post_logout_redirect_uris = oidc_json_array();
209
0
    oidc_json_array_append_new(post_logout_redirect_uris, oidc_json_string(oidc_util_url_abs(
210
0
                    r, cfg, oidc_cfg_default_slo_url_get(cfg))));
211
0
    oidc_json_object_set_new(data, OIDC_METADATA_POST_LOGOUT_REDIRECT_URIS, post_logout_redirect_uris);
212
0
  }
213
214
  /* add any custom JSON in to the registration request */
215
0
  if (oidc_cfg_provider_registration_endpoint_json_get(provider) != NULL) {
216
0
    oidc_json_t *json = NULL;
217
0
    if (oidc_json_decode_object(r, oidc_cfg_provider_registration_endpoint_json_get(provider), &json) ==
218
0
        FALSE) {
219
0
      oidc_json_decref(data);
220
0
      return FALSE;
221
0
    }
222
0
    oidc_json_merge(r, json, data);
223
0
    oidc_json_decref(json);
224
0
  }
225
226
  /* dynamically register the client with the specified parameters
227
   * NB: the TLS client certificate is presented here as it is at the other endpoints, since the
228
   *     registration endpoint may be an RFC 8705 section 5 "mtls_endpoint_aliases" one */
229
0
  if (oidc_http_post_json(r, oidc_cfg_provider_registration_endpoint_url_get(provider), data, NULL,
230
0
        oidc_cfg_provider_registration_token_get(provider), NULL,
231
0
        oidc_cfg_provider_ssl_validate_server_get(provider), response, NULL, NULL,
232
0
        oidc_cfg_http_timeout_short_get(cfg), oidc_cfg_outgoing_proxy_get(cfg),
233
0
        oidc_cfg_dir_pass_cookies_get(r),
234
0
        oidc_cfg_provider_token_endpoint_tls_client_cert_get(provider),
235
0
        oidc_cfg_provider_token_endpoint_tls_client_key_get(provider),
236
0
        oidc_cfg_provider_token_endpoint_tls_client_key_pwd_get(provider)) == FALSE) {
237
0
    OIDC_METRICS_COUNTER_INC(r, cfg, OM_PROVIDER_REGISTRATION_ERROR);
238
0
    oidc_json_decref(data);
239
0
    return FALSE;
240
0
  }
241
0
  oidc_json_decref(data);
242
243
  /* decode and see if it is not an error response somehow */
244
0
  if (oidc_json_decode_and_check_error(r, *response, j_client) == FALSE) {
245
0
    OIDC_METRICS_COUNTER_INC(r, cfg, OM_PROVIDER_REGISTRATION_ERROR);
246
0
    oidc_error(r, "JSON parsing of dynamic client registration response failed");
247
0
    return FALSE;
248
0
  }
249
250
0
  return TRUE;
251
0
}
252
253
/*
254
 * see if we have client metadata and check its validity
255
 * if not, use OpenID Connect Client Registration to get it, check it and store it
256
 */
257
apr_byte_t oidc_metadata_client_get(request_rec *r, oidc_cfg_t *cfg, const char *issuer,
258
0
            const oidc_provider_t *provider, oidc_json_t **j_client) {
259
260
  /* get the full file path to the client metadata for this issuer */
261
0
  const char *client_path = oidc_metadata_client_file_path(r, issuer);
262
263
  /* if we have valid client metadata already, return it */
264
0
  if (oidc_metadata_file_read_json(r, client_path, j_client) == TRUE) {
265
0
    if (oidc_metadata_client_is_valid(r, *j_client, issuer) == TRUE)
266
0
      return TRUE;
267
    /* stale/invalid on-disk metadata: release it before re-registering overwrites *j_client */
268
0
    oidc_json_decref(*j_client);
269
0
    *j_client = NULL;
270
0
  }
271
272
  /* at this point we have no valid client metadata, see if there's a registration endpoint for this provider */
273
0
  if (oidc_cfg_provider_registration_endpoint_url_get(provider) == NULL) {
274
0
    oidc_error(r,
275
0
         "no (valid) client metadata exists for provider (%s) and provider JSON object did not "
276
0
         "contain a (valid) \"" OIDC_METADATA_REGISTRATION_ENDPOINT "\" string",
277
0
         issuer);
278
0
    return FALSE;
279
0
  }
280
281
  /* try and get client metadata by registering the client at the registration endpoint */
282
0
  char *response = NULL;
283
0
  if (oidc_metadata_client_register(r, cfg, provider, j_client, &response) == FALSE)
284
0
    return FALSE;
285
286
  /* check to see if it is valid metadata */
287
0
  if (oidc_metadata_client_is_valid(r, *j_client, issuer) == FALSE)
288
0
    return FALSE;
289
290
  /* since it is valid, write the obtained client metadata file */
291
0
  if (oidc_util_file_write(r, client_path, response) == FALSE)
292
0
    return FALSE;
293
294
0
  return TRUE;
295
0
}
296
297
/*
298
 * override the provider token endpoint auth method when the client metadata specifies one
299
 */
300
static void oidc_metadata_client_parse_token_endpoint_auth(request_rec *r, const oidc_cfg_t *cfg,
301
5.08k
                 const oidc_json_t *j_client, oidc_provider_t *provider) {
302
303
5.08k
  char *value = NULL;
304
5.08k
  oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_TOKEN_ENDPOINT_AUTH_METHOD, &value, NULL);
305
5.08k
  if (value == NULL)
306
5.07k
    return;
307
308
3
  const char *rv = oidc_cfg_provider_token_endpoint_auth_set(r->pool, cfg, provider, value);
309
3
  if (rv != NULL)
310
3
    oidc_error(r, "oidc_provider_token_endpoint_auth_set: %s", value);
311
3
}
312
313
/*
314
 * determine the provider response_type when not already set by .conf: default from the global config,
315
 * then fall back to the first entry of the client metadata "response_types" array if the configured
316
 * one is not advertised as supported
317
 */
318
static void oidc_metadata_client_parse_response_type(request_rec *r, oidc_cfg_t *cfg, const oidc_json_t *j_client,
319
5.08k
                 oidc_provider_t *provider) {
320
321
5.08k
  const char *value = NULL;
322
323
5.08k
  if (oidc_cfg_provider_response_type_is_set(provider))
324
7
    return;
325
326
5.07k
  oidc_cfg_provider_response_type_set(r->pool, provider,
327
5.07k
              oidc_cfg_provider_response_type_get(oidc_cfg_provider_get(cfg)));
328
329
  // "response_types" is an array in the client metadata as by spec
330
5.07k
  const oidc_json_t *j_response_types = oidc_json_object_get(j_client, OIDC_METADATA_RESPONSE_TYPES);
331
5.07k
  if ((j_response_types == NULL) || (!oidc_json_is_array(j_response_types)))
332
4.99k
    return;
333
334
  // if there's an array we'll prefer the configured response_type if supported
335
80
  if (oidc_json_array_has_value(r, j_response_types, oidc_cfg_provider_response_type_get(provider)) == TRUE)
336
13
    return;
337
338
  // if the configured response_type is not supported, we'll fallback to the first one that is listed
339
67
  const oidc_json_t *j_response_type = oidc_json_array_get(j_response_types, 0);
340
67
  if (oidc_json_is_string(j_response_type)) {
341
39
    value = apr_pstrdup(r->pool, oidc_json_string_value(j_response_type));
342
39
    OIDC_METADATA_PROVIDER_SET(response_type, value);
343
39
  }
344
67
}
345
346
/*
347
 * parse the JSON client metadata in to a oidc_provider_t struct
348
 */
349
apr_byte_t oidc_metadata_client_parse(request_rec *r, oidc_cfg_t *cfg, const oidc_json_t *j_client,
350
5.08k
              oidc_provider_t *provider) {
351
352
5.08k
  char *value = NULL;
353
354
  /* get a handle to the client_id we need to use for this provider */
355
5.08k
  oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_ID, &value, NULL);
356
5.08k
  OIDC_METADATA_PROVIDER_SET(client_id, value);
357
358
  /* get a handle to the client_secret we need to use for this provider */
359
5.08k
  oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_SECRET, &value, NULL);
360
5.08k
  OIDC_METADATA_PROVIDER_SET(client_secret, value);
361
362
  /* see if the token endpoint auth method defined in the client metadata overrides the provider one */
363
5.08k
  oidc_metadata_client_parse_token_endpoint_auth(r, cfg, j_client, provider);
364
365
  /* determine the response type if not set by .conf */
366
5.08k
  oidc_metadata_client_parse_response_type(r, cfg, j_client, provider);
367
368
5.08k
  oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_ID_TOKEN_SIGNED_RESPONSE_ALG, &value,
369
5.08k
            oidc_cfg_provider_id_token_signed_response_alg_get(oidc_cfg_provider_get(cfg)));
370
5.08k
  OIDC_METADATA_PROVIDER_SET(id_token_signed_response_alg, value);
371
372
  // NB: id_token_encrypted_response_alg and the other encryption-related client metadata are not parsed here
373
374
5.08k
  return TRUE;
375
5.08k
}