Coverage Report

Created: 2026-09-01 06:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mod_auth_openidc/src/handle/discovery.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
 * All rights reserved.
23
 *
24
 * DISCLAIMER OF WARRANTIES:
25
 *
26
 * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
27
 * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
28
 * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
29
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  NOR ARE THERE ANY
30
 * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
31
 * USAGE.  FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
32
 * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
33
 * WILL BE UNINTERRUPTED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
34
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
35
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
36
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
37
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
38
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
 *
40
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
41
 */
42
43
#include "cfg/dir.h"
44
#include "handle/handle.h"
45
#include "metadata.h"
46
#include "mod_auth_openidc.h"
47
#include "proto/proto.h"
48
#include "util/util.h"
49
#include "util/util_cfg.h"
50
51
#include <apr_lib.h>
52
53
/* parameter name of the callback URL in the discovery response */
54
0
#define OIDC_DISC_CB_PARAM "oidc_callback"
55
/* parameter name of the OP provider selection in the discovery response */
56
7.81k
#define OIDC_DISC_OP_PARAM "iss"
57
/* parameter name of the user URL in the discovery response */
58
6.93k
#define OIDC_DISC_USER_PARAM "disc_user"
59
/* parameter name of the original URL in the discovery response */
60
5.82k
#define OIDC_DISC_RT_PARAM "target_link_uri"
61
/* parameter name of login hint in the discovery response */
62
5.82k
#define OIDC_DISC_LH_PARAM "login_hint"
63
/* parameter name of parameters that need to be passed in the authentication request */
64
5.82k
#define OIDC_DISC_AR_PARAM "auth_request_params"
65
/* parameter name of the scopes required in the discovery response */
66
5.82k
#define OIDC_DISC_SC_PARAM "scopes"
67
68
/*
69
 * find out whether the request is a response from an IDP discovery page
70
 */
71
1.99k
apr_byte_t oidc_is_discovery_response(request_rec *r, oidc_cfg_t *cfg) {
72
  /*
73
   * prereq: this is a call to the configured redirect_uri, now see if:
74
   * the OIDC_DISC_OP_PARAM is present
75
   */
76
1.99k
  return oidc_util_url_has_parameter(r, OIDC_DISC_OP_PARAM) ||
77
1.11k
         oidc_util_url_has_parameter(r, OIDC_DISC_USER_PARAM);
78
1.99k
}
79
80
0
static const char *oidc_discovery_csrf_cookie_samesite(const request_rec *r, const oidc_cfg_t *c) {
81
0
  const char *rv = NULL;
82
0
  switch (oidc_cfg_cookie_same_site_discovery_csrf_get(c)) {
83
0
  case OIDC_SAMESITE_COOKIE_STRICT:
84
0
    rv = OIDC_HTTP_COOKIE_SAMESITE_STRICT;
85
0
    break;
86
0
  case OIDC_SAMESITE_COOKIE_LAX:
87
0
    rv = OIDC_HTTP_COOKIE_SAMESITE_LAX;
88
0
    break;
89
0
  case OIDC_SAMESITE_COOKIE_NONE:
90
0
    rv = OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r);
91
0
    break;
92
0
  case OIDC_SAMESITE_COOKIE_DISABLED:
93
0
    break;
94
0
  default:
95
0
    break;
96
0
  }
97
0
  return rv;
98
0
}
99
100
/* define the name of the cookie/parameter for CSRF protection */
101
7.07k
#define OIDC_CSRF_NAME "x_csrf"
102
103
/*
104
 * redirect the user to an external discovery page, passing the required parameters
105
 */
106
static int oidc_discovery_request_external(request_rec *r, const oidc_cfg_t *cfg, const char *discover_url,
107
0
             const char *current_url, const char *method, const char *csrf) {
108
109
0
  const char *path_scopes = oidc_cfg_dir_path_scope_get(r);
110
0
  const char *path_auth_request_params = oidc_cfg_dir_path_auth_request_params_get(r);
111
112
  /* assemble the parameters for external discovery */
113
0
  char *url = apr_psprintf(r->pool, "%s%s%s=%s&%s=%s&%s=%s&%s=%s", discover_url,
114
0
         strchr(discover_url, OIDC_CHAR_QUERY) != NULL ? OIDC_STR_AMP : OIDC_STR_QUERY,
115
0
         OIDC_DISC_RT_PARAM, oidc_http_url_encode(r, current_url), OIDC_DISC_RM_PARAM, method,
116
0
         OIDC_DISC_CB_PARAM, oidc_http_url_encode(r, oidc_util_url_redirect_uri(r, cfg)),
117
0
         OIDC_CSRF_NAME, oidc_http_url_encode(r, csrf));
118
119
0
  if (path_scopes != NULL)
120
0
    url = apr_psprintf(r->pool, "%s&%s=%s", url, OIDC_DISC_SC_PARAM, oidc_http_url_encode(r, path_scopes));
121
0
  if (path_auth_request_params != NULL)
122
0
    url = apr_psprintf(r->pool, "%s&%s=%s", url, OIDC_DISC_AR_PARAM,
123
0
           oidc_http_url_encode(r, path_auth_request_params));
124
125
  /* log what we're about to do */
126
0
  oidc_debug(r, "redirecting to external discovery page: %s", url);
127
128
  /* set CSRF cookie */
129
0
  oidc_http_set_cookie(r, OIDC_CSRF_NAME, csrf, -1, oidc_discovery_csrf_cookie_samesite(r, cfg));
130
131
  /* see if we need to preserve POST parameters through Javascript/HTML5 storage */
132
0
  if (oidc_response_post_preserve_javascript(r, url, NULL, NULL) == TRUE)
133
0
    return OK;
134
135
  /* do the actual redirect to an external discovery page */
136
0
  oidc_http_hdr_out_location_set(r, url);
137
138
0
  return HTTP_MOVED_TEMPORARILY;
139
0
}
140
141
/*
142
 * append the list of statically configured providers as selection links to the discovery page
143
 */
144
static const char *oidc_discovery_page_providers(request_rec *r, const oidc_cfg_t *cfg, const apr_array_header_t *arr,
145
             const char *current_url, const char *method, const char *csrf,
146
0
             const char *s) {
147
148
0
  const char *path_scopes = oidc_cfg_dir_path_scope_get(r);
149
0
  const char *path_auth_request_params = oidc_cfg_dir_path_auth_request_params_get(r);
150
151
0
  for (int i = 0; i < arr->nelts; i++) {
152
153
0
    const char *issuer = APR_ARRAY_IDX(arr, i, const char *);
154
155
    /* Escape the unencoded redirect URI only; escaping the full href would double-encode &amp;. */
156
0
    char *href = apr_psprintf(r->pool, "%s?%s=%s&amp;%s=%s&amp;%s=%s&amp;%s=%s",
157
0
            oidc_util_html_escape(r->pool, oidc_util_url_redirect_uri(r, cfg)),
158
0
            OIDC_DISC_OP_PARAM, oidc_http_url_encode(r, issuer), OIDC_DISC_RT_PARAM,
159
0
            oidc_http_url_encode(r, current_url), OIDC_DISC_RM_PARAM, method,
160
0
            OIDC_CSRF_NAME, csrf);
161
162
0
    if (path_scopes != NULL)
163
0
      href = apr_psprintf(r->pool, "%s&amp;%s=%s", href, OIDC_DISC_SC_PARAM,
164
0
              oidc_http_url_encode(r, path_scopes));
165
0
    if (path_auth_request_params != NULL)
166
0
      href = apr_psprintf(r->pool, "%s&amp;%s=%s", href, OIDC_DISC_AR_PARAM,
167
0
              oidc_http_url_encode(r, path_auth_request_params));
168
169
0
    const char *display = (_oidc_strstr(issuer, "https://") == NULL)
170
0
            ? apr_pstrdup(r->pool, issuer)
171
0
            : apr_pstrdup(r->pool, issuer + _oidc_strlen("https://"));
172
173
    /* strip port number */
174
    /* point back to the redirect_uri, where the selection is handled, with an IDP selection and return_to
175
     * URL */
176
0
    s = apr_psprintf(r->pool, "%s<p><a href=\"%s\">%s</a></p>\n", s, href,
177
0
         oidc_util_html_escape(r->pool, display));
178
0
  }
179
180
0
  return s;
181
0
}
182
183
/*
184
 * append the form to enter an account or issuer name for dynamic OP discovery to the discovery page
185
 */
186
static const char *oidc_discovery_page_form(request_rec *r, const oidc_cfg_t *cfg, const char *current_url,
187
0
              const char *method, const char *csrf, const char *s) {
188
189
0
  const char *path_scopes = oidc_cfg_dir_path_scope_get(r);
190
0
  const char *path_auth_request_params = oidc_cfg_dir_path_auth_request_params_get(r);
191
192
  /* same as the provider links above: the redirect URI is the only value here that does not
193
   * arrive escaped, and a relative OIDCRedirectURI resolves it against the request's host */
194
0
  s = apr_psprintf(r->pool, "%s<form method=\"get\" action=\"%s\">\n", s,
195
0
       oidc_util_html_escape(r->pool, oidc_util_url_redirect_uri(r, cfg)));
196
0
  s = apr_psprintf(r->pool, "%s<p><input type=\"hidden\" name=\"%s\" value=\"%s\"><p>\n", s, OIDC_DISC_RT_PARAM,
197
0
       oidc_util_html_escape(r->pool, current_url));
198
0
  s = apr_psprintf(r->pool, "%s<p><input type=\"hidden\" name=\"%s\" value=\"%s\"><p>\n", s, OIDC_DISC_RM_PARAM,
199
0
       oidc_util_html_escape(r->pool, method));
200
0
  s = apr_psprintf(r->pool, "%s<p><input type=\"hidden\" name=\"%s\" value=\"%s\"><p>\n", s, OIDC_CSRF_NAME,
201
0
       oidc_util_html_escape(r->pool, csrf));
202
203
0
  if (path_scopes != NULL)
204
0
    s = apr_psprintf(r->pool, "%s<p><input type=\"hidden\" name=\"%s\" value=\"%s\"><p>\n", s,
205
0
         OIDC_DISC_SC_PARAM, oidc_util_html_escape(r->pool, path_scopes));
206
0
  if (path_auth_request_params != NULL)
207
0
    s = apr_psprintf(r->pool, "%s<p><input type=\"hidden\" name=\"%s\" value=\"%s\"><p>\n", s,
208
0
         OIDC_DISC_AR_PARAM, oidc_util_html_escape(r->pool, path_auth_request_params));
209
210
0
  s = apr_psprintf(r->pool,
211
0
       "%s<p>Or enter your account name (eg. &quot;mike@seed.gluu.org&quot;, or an IDP identifier "
212
0
       "(eg. &quot;mitreid.org&quot;):</p>\n",
213
0
       s);
214
0
  s = apr_psprintf(r->pool, "%s<p><input type=\"text\" name=\"%s\" value=\"%s\"></p>\n", s, OIDC_DISC_OP_PARAM,
215
0
       "");
216
0
  s = apr_psprintf(r->pool, "%s<p><input type=\"submit\" value=\"Submit\"></p>\n", s);
217
0
  s = apr_psprintf(r->pool, "%s</form>\n", s);
218
219
0
  return s;
220
0
}
221
222
/*
223
 * present the user with an OP selection screen
224
 */
225
0
int oidc_discovery_request(request_rec *r, oidc_cfg_t *cfg) {
226
227
0
  oidc_debug(r, "enter");
228
229
  /* obtain the URL we're currently accessing, to be stored in the state/session */
230
0
  const char *current_url = oidc_util_url_cur(r, oidc_cfg_x_forwarded_headers_get(cfg));
231
0
  const char *method = oidc_original_request_method(r, cfg, FALSE);
232
233
  /* generate CSRF token; 16 bytes (128 bits) of entropy */
234
0
  char *csrf = NULL;
235
0
  if (oidc_util_rand_str(r, &csrf, 16) == FALSE)
236
0
    return HTTP_INTERNAL_SERVER_ERROR;
237
238
  /* see if there's an external discovery page configured */
239
0
  const char *discover_url = oidc_cfg_dir_discover_url_get(r);
240
0
  if (discover_url != NULL)
241
0
    return oidc_discovery_request_external(r, cfg, discover_url, current_url, method, csrf);
242
243
  /* get a list of all providers configured in the metadata directory */
244
0
  apr_array_header_t *arr = NULL;
245
0
  if (oidc_metadata_list(r, cfg, &arr) == FALSE)
246
0
    return oidc_util_html_send_error(r, "Configuration Error",
247
0
             "No configured providers found, contact your administrator",
248
0
             HTTP_UNAUTHORIZED);
249
250
  /* assemble a where-are-you-from IDP discovery HTML page */
251
0
  const char *s = "\t\t\t<h3>Select your OpenID Connect Identity Provider</h3>\n";
252
253
  /* list all configured providers in there */
254
0
  s = oidc_discovery_page_providers(r, cfg, arr, current_url, method, csrf, s);
255
256
  /* add an option to enter an account or issuer name for dynamic OP discovery */
257
0
  s = oidc_discovery_page_form(r, cfg, current_url, method, csrf, s);
258
259
0
  oidc_http_set_cookie(r, OIDC_CSRF_NAME, csrf, -1, oidc_discovery_csrf_cookie_samesite(r, cfg));
260
261
0
  char *javascript = NULL;
262
0
  char *javascript_method = NULL;
263
0
  char *html_head = "<style type=\"text/css\">body {text-align: center}</style>";
264
0
  if (oidc_response_post_preserve_javascript(r, NULL, &javascript, &javascript_method) == TRUE)
265
0
    html_head = apr_psprintf(r->pool, "%s%s", html_head, javascript);
266
267
  /* now send the HTML contents to the user agent */
268
0
  return oidc_util_html_send(r, "OpenID Connect Provider Discovery", html_head, javascript_method, s, OK);
269
0
}
270
271
/*
272
 * check if the target_link_uri matches to configuration settings to prevent an open redirect
273
 */
274
5.10k
static int oidc_discovery_target_link_uri_match(request_rec *r, const oidc_cfg_t *cfg, const char *target_link_uri) {
275
276
5.10k
  apr_uri_t o_uri;
277
5.10k
  apr_uri_parse(r->pool, target_link_uri, &o_uri);
278
5.10k
  if (o_uri.hostname == NULL) {
279
5
    oidc_error(r, "could not parse the \"target_link_uri\" (%s) in to a valid URL: aborting.",
280
5
         target_link_uri);
281
5
    return FALSE;
282
5
  }
283
284
5.09k
  apr_uri_t r_uri;
285
5.09k
  apr_uri_parse(r->pool, oidc_util_url_redirect_uri(r, cfg), &r_uri);
286
287
5.09k
  if (oidc_cfg_cookie_domain_get(cfg) == NULL) {
288
    /* no cookie_domain set: target_link_uri host must be equal to, or a subdomain of, the redirect_uri host
289
     * (because that's where the session cookie will be set) */
290
5.09k
    if (oidc_util_hostname_endswith(o_uri.hostname, r_uri.hostname) == FALSE) {
291
166
      oidc_error(r,
292
166
           "the URL hostname (%s) of the configured " OIDCRedirectURI
293
166
           " does not match the URL hostname of the \"target_link_uri\" (%s): aborting "
294
166
           "to prevent an open redirect.",
295
166
           r_uri.hostname, o_uri.hostname);
296
166
      return FALSE;
297
166
    }
298
5.09k
  } else {
299
    /* cookie_domain set: see if the target_link_uri is within the cookie_domain */
300
0
    if (oidc_util_cookie_domain_valid(o_uri.hostname, oidc_cfg_cookie_domain_get(cfg)) == FALSE) {
301
0
      oidc_error(r,
302
0
           "the domain (%s) configured in " OIDCCookieDomain
303
0
           " does not match the URL hostname (%s) of the \"target_link_uri\" (%s): aborting to "
304
0
           "prevent an open redirect.",
305
0
           oidc_cfg_cookie_domain_get(cfg), o_uri.hostname, target_link_uri);
306
0
      return FALSE;
307
0
    }
308
0
  }
309
310
  /* see if the cookie_path setting matches the target_link_uri path */
311
4.93k
  const char *cookie_path = oidc_cfg_dir_cookie_path_get(r);
312
4.93k
  if (cookie_path != NULL) {
313
4.93k
    const char *p = (o_uri.path != NULL) ? _oidc_strstr(o_uri.path, cookie_path) : NULL;
314
4.93k
    if (p != o_uri.path) {
315
0
      oidc_error(r,
316
0
           "the path (%s) configured in " OIDCCookiePath
317
0
           " does not match the URL path (%s) of the \"target_link_uri\" (%s): aborting to "
318
0
           "prevent an open redirect.",
319
0
           cookie_path, o_uri.path, target_link_uri);
320
0
      return FALSE;
321
4.93k
    } else if (_oidc_strlen(o_uri.path) > _oidc_strlen(cookie_path)) {
322
2.27k
      int n = (int)_oidc_strlen(cookie_path);
323
2.27k
      if ((n > 0) && (cookie_path[n - 1] == OIDC_CHAR_FORWARD_SLASH))
324
2.27k
        n--;
325
2.27k
      if (o_uri.path[n] != OIDC_CHAR_FORWARD_SLASH) {
326
0
        oidc_error(r,
327
0
             "the path (%s) configured in " OIDCCookiePath
328
0
             " does not match the URL path (%s) of the \"target_link_uri\" (%s): "
329
0
             "aborting to prevent an open redirect.",
330
0
             cookie_path, o_uri.path, target_link_uri);
331
0
        return FALSE;
332
0
      }
333
2.27k
    }
334
4.93k
  }
335
4.93k
  return TRUE;
336
4.93k
}
337
338
/*
339
 * verify CSRF protection for the discovery response; returns TRUE when a
340
 * valid CSRF cookie/query pair was found (user-initiated discovery, dynamic
341
 * client registration allowed), FALSE for 3rd-party initiated SSO or when
342
 * the CSRF check fails
343
 */
344
5.82k
static apr_byte_t oidc_discovery_response_csrf_check(request_rec *r, const oidc_cfg_t *c) {
345
346
5.82k
  const char *csrf_cookie = oidc_http_get_cookie(r, OIDC_CSRF_NAME);
347
5.82k
  char *csrf_query = NULL;
348
349
  /* no CSRF cookie means this is 3rd party initiated SSO */
350
5.82k
  if (csrf_cookie == NULL)
351
5.20k
    return FALSE;
352
353
  /* clean CSRF cookie */
354
624
  oidc_http_set_cookie(r, OIDC_CSRF_NAME, "", 0, OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r));
355
356
  /* compare CSRF cookie value with query parameter value */
357
624
  oidc_util_url_parameter_get(r, OIDC_CSRF_NAME, &csrf_query);
358
624
  if (oidc_util_strcmp_const_time(csrf_query, csrf_cookie) == FALSE) {
359
54
    oidc_warn(r, "CSRF protection failed, no Discovery and dynamic client registration will be allowed");
360
54
    return FALSE;
361
54
  }
362
363
570
  return TRUE;
364
624
}
365
366
/*
367
 * apply the default and validate the target_link_uri; on failure sets
368
 * *rv to the HTTP status the caller should return
369
 */
370
static apr_byte_t oidc_discovery_response_target_link_uri_validate(request_rec *r, const oidc_cfg_t *c,
371
5.82k
                   char **target_link_uri, int *rv) {
372
373
5.82k
  char *error_str = NULL;
374
5.82k
  char *error_description = NULL;
375
376
5.82k
  if (*target_link_uri == NULL) {
377
723
    if (oidc_cfg_default_sso_url_get(c) == NULL) {
378
723
      *rv = oidc_util_html_send_error(r, "Invalid Request",
379
723
              "SSO to this module without specifying a \"target_link_uri\" "
380
723
              "parameter is not possible because " OIDCDefaultURL
381
723
              " is not set.",
382
723
              HTTP_INTERNAL_SERVER_ERROR);
383
723
      return FALSE;
384
723
    }
385
0
    *target_link_uri = apr_pstrdup(r->pool, oidc_util_url_abs(r, c, oidc_cfg_default_sso_url_get(c)));
386
0
  }
387
388
  /* do open redirect prevention, step 1 */
389
5.10k
  if (oidc_discovery_target_link_uri_match(r, c, *target_link_uri) == FALSE) {
390
171
    *rv = oidc_util_html_send_error(r, "Invalid Request",
391
171
            "\"target_link_uri\" parameter does not match configuration settings, "
392
171
            "aborting to prevent an open redirect.",
393
171
            HTTP_UNAUTHORIZED);
394
171
    return FALSE;
395
171
  }
396
397
  /* do input validation on the target_link_uri parameter value, step 2 */
398
4.93k
  if (oidc_validate_redirect_url(r, c, *target_link_uri, OIDC_REDIRECT_URL_SAME_HOST, &error_str,
399
4.93k
               &error_description) == FALSE) {
400
50
    *rv = oidc_util_html_send_error(r, error_str, error_description, HTTP_UNAUTHORIZED);
401
50
    return FALSE;
402
50
  }
403
404
4.88k
  return TRUE;
405
4.93k
}
406
407
/*
408
 * handle a static (single-OP) configuration: optionally validate that the
409
 * supplied issuer matches the configured one, then trigger authentication
410
 */
411
static int oidc_discovery_response_static(request_rec *r, oidc_cfg_t *c, const char *issuer,
412
            const char *target_link_uri, const char *login_hint,
413
4.88k
            const char *auth_request_params, const char *path_scopes) {
414
415
4.88k
  oidc_provider_t *provider = NULL;
416
4.88k
  if ((oidc_provider_static_config(r, c, &provider) == TRUE) && (issuer != NULL) &&
417
583
      (_oidc_strcmp(oidc_cfg_provider_issuer_get(provider), issuer) != 0)) {
418
301
    return oidc_util_html_send_error(
419
301
        r, "Invalid Request",
420
301
        apr_psprintf(r->pool, "The \"iss\" value must match the configured providers' one (%s != %s).",
421
301
         issuer, oidc_cfg_provider_issuer_get(oidc_cfg_provider_get(c))),
422
301
        HTTP_INTERNAL_SERVER_ERROR);
423
301
  }
424
425
4.57k
  return oidc_request_authenticate_user(r, c, NULL, target_link_uri, login_hint, NULL, NULL, auth_request_params,
426
4.57k
                path_scopes);
427
4.88k
}
428
429
/*
430
 * verify the issuer matches one of the OIDCDiscoverIssuersAllowed regexes, when configured;
431
 * this bounds the set of hosts that a client-driven Discovery request (webfinger/URL-based,
432
 * account-based, or direct issuer selection) can cause the server to make outbound requests to
433
 */
434
0
static apr_byte_t oidc_discovery_issuer_allowed(request_rec *r, const oidc_cfg_t *c, const char *issuer) {
435
0
  apr_hash_t *allowed = oidc_cfg_discover_issuers_allowed_get(c);
436
0
  const char *pattern = NULL;
437
0
  char *error_str = NULL;
438
439
0
  if (allowed == NULL)
440
0
    return TRUE;
441
442
0
  for (apr_hash_index_t *hi = apr_hash_first(NULL, allowed); hi; hi = apr_hash_next(hi)) {
443
0
    apr_hash_this(hi, (const void **)&pattern, NULL, NULL);
444
0
    if (oidc_util_regexp_first_match(r->pool, issuer, pattern, NULL, &error_str) == TRUE)
445
0
      return TRUE;
446
0
  }
447
448
0
  oidc_warn(r, "issuer (%s) does not match the list of allowed Discovery issuers", issuer);
449
0
  return FALSE;
450
0
}
451
452
/*
453
 * resolve the issuer for user-identifier or account-name based discovery;
454
 * on failure sets *rv to the HTTP status the caller should return
455
 */
456
static apr_byte_t oidc_discovery_response_resolve_issuer(request_rec *r, oidc_cfg_t *c, char *user, char **issuer,
457
0
               char **login_hint, int *rv) {
458
459
0
  if (user != NULL) {
460
461
0
    if (*login_hint == NULL)
462
0
      *login_hint = apr_pstrdup(r->pool, user);
463
464
    /* normalize the user identifier */
465
0
    if (_oidc_strstr(user, "https://") != user)
466
0
      user = apr_psprintf(r->pool, "https://%s", user);
467
468
    /* enforce the issuer allow-list *before* the webfinger discovery HTTP call itself
469
     * (rather than only against the issuer it resolves to): otherwise a disallowed host
470
     * could still be probed with an outbound request even though the response would
471
     * ultimately be rejected */
472
0
    if (oidc_discovery_issuer_allowed(r, c, user) == FALSE) {
473
0
      *rv = oidc_util_html_send_error(
474
0
          r, "Invalid Request",
475
0
          "The provided user identifier is not in the list of allowed issuers; contact the "
476
0
          "administrator",
477
0
          HTTP_UNAUTHORIZED);
478
0
      return FALSE;
479
0
    }
480
481
    /* got a user identifier as input, perform OP discovery with that */
482
0
    if (oidc_proto_discovery_url_based(r, c, user, issuer) == FALSE) {
483
0
      *rv = oidc_util_html_send_error(r, "Invalid Request",
484
0
              "Could not resolve the provided user identifier to an OpenID "
485
0
              "Connect provider; check your syntax.",
486
0
              HTTP_NOT_FOUND);
487
0
      return FALSE;
488
0
    }
489
490
0
    return TRUE;
491
0
  }
492
493
0
  if (_oidc_strstr(*issuer, OIDC_STR_AT) != NULL) {
494
495
0
    if (*login_hint == NULL)
496
0
      *login_hint = apr_pstrdup(r->pool, *issuer);
497
498
    /* same reasoning as above: gate the domain that account-based (webfinger) discovery
499
     * would otherwise probe, before the outbound HTTP call is made */
500
0
    const char *domain = strrchr(*issuer, OIDC_CHAR_AT);
501
0
    const char *domain_issuer = apr_psprintf(r->pool, "https://%s", domain ? domain + 1 : *issuer);
502
0
    if (oidc_discovery_issuer_allowed(r, c, domain_issuer) == FALSE) {
503
0
      *rv = oidc_util_html_send_error(
504
0
          r, "Invalid Request",
505
0
          "The provided account name is not in the list of allowed issuers; contact the "
506
0
          "administrator",
507
0
          HTTP_UNAUTHORIZED);
508
0
      return FALSE;
509
0
    }
510
511
    /* got an account name as input, perform OP discovery with that */
512
0
    if (oidc_proto_discovery_account_based(r, c, *issuer, issuer) == FALSE) {
513
0
      *rv = oidc_util_html_send_error(r, "Invalid Request",
514
0
              "Could not resolve the provided account name to an OpenID "
515
0
              "Connect provider; check your syntax.",
516
0
              HTTP_NOT_FOUND);
517
0
      return FALSE;
518
0
    }
519
0
  }
520
521
0
  return TRUE;
522
0
}
523
524
/*
525
 * post-discovery: handle the test-config / test-jwks-uri short-circuits or
526
 * trigger authentication with the resolved provider
527
 */
528
static int oidc_discovery_response_authenticate(request_rec *r, oidc_cfg_t *c, char *issuer,
529
            const char *target_link_uri, const char *login_hint,
530
            const char *auth_request_params, const char *path_scopes,
531
0
            apr_byte_t allow_dyn_reg) {
532
533
  /* strip trailing '/' */
534
0
  int n = (int)_oidc_strlen(issuer);
535
0
  if ((n > 0) && (issuer[n - 1] == OIDC_CHAR_FORWARD_SLASH))
536
0
    issuer[n - 1] = '\0';
537
538
0
  if (oidc_discovery_issuer_allowed(r, c, issuer) == FALSE)
539
0
    return oidc_util_html_send_error(
540
0
        r, "Invalid Request",
541
0
        "The selected OpenID Connect provider issuer is not in the list of allowed issuers; "
542
0
        "contact the administrator",
543
0
        HTTP_UNAUTHORIZED);
544
545
0
  if (oidc_util_url_has_parameter(r, "test-config")) {
546
0
    oidc_json_t *j_provider = NULL;
547
0
    oidc_metadata_provider_get(r, c, issuer, &j_provider, allow_dyn_reg);
548
0
    if (j_provider)
549
0
      oidc_json_decref(j_provider);
550
0
    return OK;
551
0
  }
552
553
  /* try and get metadata from the metadata directories for the selected OP */
554
0
  oidc_provider_t *provider = NULL;
555
0
  if ((oidc_metadata_get(r, c, issuer, &provider, allow_dyn_reg) == FALSE) || (provider == NULL))
556
0
    return oidc_util_html_send_error(
557
0
        r, "Invalid Request",
558
0
        "Could not find valid provider metadata for the selected OpenID Connect "
559
0
        "provider; contact the administrator",
560
0
        HTTP_NOT_FOUND);
561
562
0
  if (oidc_util_url_has_parameter(r, "test-jwks-uri")) {
563
0
    oidc_json_t *j_jwks = NULL;
564
0
    apr_byte_t force_refresh = TRUE;
565
0
    oidc_metadata_jwks_get(r, c, oidc_cfg_provider_jwks_uri_get(provider),
566
0
               oidc_cfg_provider_ssl_validate_server_get(provider), &j_jwks, &force_refresh);
567
0
    oidc_json_decref(j_jwks);
568
0
    return OK;
569
0
  }
570
571
  /* now we've got a selected OP, send the user there to authenticate */
572
0
  return oidc_request_authenticate_user(r, c, provider, target_link_uri, login_hint, NULL, NULL,
573
0
                auth_request_params, path_scopes);
574
0
}
575
576
/*
577
 * strip leading/trailing whitespace from a user-typed discovery input value, to be
578
 * tolerant of stray whitespace in copy-pasted issuer/account values
579
 */
580
11.6k
static char *oidc_discovery_response_input_trim(char *value) {
581
11.6k
  char *p = NULL;
582
11.6k
  if (value == NULL)
583
10.2k
    return NULL;
584
2.49k
  while (apr_isspace(*value))
585
1.05k
    value++;
586
1.43k
  p = value + _oidc_strlen(value);
587
2.98k
  while ((p > value) && (apr_isspace(*(p - 1))))
588
1.55k
    *(--p) = '\0';
589
1.43k
  return value;
590
11.6k
}
591
592
/*
593
 * handle a response from an IDP discovery page and/or handle 3rd-party initiated SSO
594
 */
595
5.82k
int oidc_discovery_response(request_rec *r, oidc_cfg_t *c) {
596
597
5.82k
  char *issuer = NULL;
598
5.82k
  char *target_link_uri = NULL;
599
5.82k
  char *login_hint = NULL;
600
5.82k
  char *auth_request_params = NULL;
601
5.82k
  char *user = NULL;
602
5.82k
  char *path_scopes = NULL;
603
5.82k
  int rv = OK;
604
605
5.82k
  oidc_util_url_parameter_get(r, OIDC_DISC_OP_PARAM, &issuer);
606
5.82k
  oidc_util_url_parameter_get(r, OIDC_DISC_USER_PARAM, &user);
607
5.82k
  oidc_util_url_parameter_get(r, OIDC_DISC_RT_PARAM, &target_link_uri);
608
5.82k
  oidc_util_url_parameter_get(r, OIDC_DISC_LH_PARAM, &login_hint);
609
5.82k
  oidc_util_url_parameter_get(r, OIDC_DISC_SC_PARAM, &path_scopes);
610
5.82k
  oidc_util_url_parameter_get(r, OIDC_DISC_AR_PARAM, &auth_request_params);
611
612
  /* do CSRF protection if not 3rd party initiated SSO */
613
5.82k
  apr_byte_t csrf_valid = oidc_discovery_response_csrf_check(r, c);
614
615
  /* Only a CSRF-validated Discovery response may control scopes or authorization parameters. */
616
5.82k
  if (csrf_valid == FALSE) {
617
5.25k
    if ((path_scopes != NULL) || (auth_request_params != NULL))
618
5.25k
      oidc_warn(r,
619
5.25k
          "ignoring the \"%s\" and/or \"%s\" parameter(s): they are only honoured on a "
620
5.25k
          "Discovery response that passes CSRF validation, not on 3rd-party initiated SSO",
621
5.25k
          OIDC_DISC_SC_PARAM, OIDC_DISC_AR_PARAM);
622
5.25k
    path_scopes = NULL;
623
5.25k
    auth_request_params = NULL;
624
5.25k
  }
625
626
  /* the issuer/account-name values may have been typed/pasted by the user on the discovery page */
627
5.82k
  issuer = oidc_discovery_response_input_trim(issuer);
628
5.82k
  user = oidc_discovery_response_input_trim(user);
629
630
5.82k
  oidc_debug(r, "issuer=\"%s\", target_link_uri=\"%s\", login_hint=\"%s\", user=\"%s\"", issuer, target_link_uri,
631
5.82k
       login_hint, user);
632
633
5.82k
  if (oidc_discovery_response_target_link_uri_validate(r, c, &target_link_uri, &rv) == FALSE)
634
944
    return rv;
635
636
  /* see if this is a static setup */
637
4.88k
  if (oidc_cfg_metadata_dir_get(c) == NULL)
638
4.88k
    return oidc_discovery_response_static(r, c, issuer, target_link_uri, login_hint, auth_request_params,
639
4.88k
                  path_scopes);
640
641
  /* find out if the user entered an account name or selected an OP manually */
642
0
  if (oidc_discovery_response_resolve_issuer(r, c, user, &issuer, &login_hint, &rv) == FALSE)
643
0
    return rv;
644
645
0
  return oidc_discovery_response_authenticate(r, c, issuer, target_link_uri, login_hint, auth_request_params,
646
0
                path_scopes, csrf_valid);
647
0
}