Coverage Report

Created: 2026-09-27 07:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mod_auth_openidc/src/handle/userinfo.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 "cfg/provider.h"
45
#include "handle/handle.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
/*
52
 * store claims resolved from the userinfo endpoint in the session
53
 */
54
void oidc_userinfo_store_claims(request_rec *r, const oidc_cfg_t *c, oidc_session_t *session,
55
        const oidc_provider_t *provider, oidc_json_t *userinfo_claims,
56
416
        const char *userinfo_jwt) {
57
58
416
  oidc_debug(r, "enter");
59
60
  /* see if we've resolved any claims */
61
416
  if (userinfo_claims != NULL) {
62
    /*
63
     * Successfully decoded a set claims from the response so we can store them
64
     * (well actually the stringified representation in the response)
65
     * in the session context safely now
66
     */
67
0
    oidc_session_set_userinfo_claims(r, session, userinfo_claims);
68
69
0
    if (oidc_cfg_session_type_get(c) != OIDC_SESSION_TYPE_CLIENT_COOKIE) {
70
      /* this will also clear the entry if a JWT was not returned at this point */
71
0
      oidc_session_set_userinfo_jwt(r, session, userinfo_jwt);
72
0
    }
73
74
416
  } else {
75
    /*
76
     * clear the existing claims because we could not refresh them
77
     */
78
416
    oidc_session_set_userinfo_claims(r, session, NULL);
79
80
416
    oidc_session_set_userinfo_jwt(r, session, NULL);
81
416
  }
82
83
  /* store the last refresh time if we've configured a userinfo refresh interval */
84
416
  if (oidc_cfg_provider_userinfo_refresh_interval_get(provider) > -1)
85
0
    oidc_session_reset_userinfo_last_refresh(r, session);
86
416
}
87
88
/*
89
 * retrieve claims from the userinfo endpoint and return the stringified response
90
 */
91
const char *oidc_userinfo_retrieve_claims(request_rec *r, oidc_cfg_t *c, const oidc_provider_t *provider,
92
            const char *access_token, const char *access_token_type,
93
            oidc_session_t *session, char *id_token_sub, oidc_json_t **userinfo_claims,
94
416
            char **userinfo_jwt) {
95
96
416
  char *result = NULL;
97
416
  char *refreshed_access_token = NULL;
98
416
  char *refreshed_access_token_type = NULL;
99
416
  const oidc_json_t *id_token_claims = NULL;
100
416
  long response_code = 0;
101
102
416
  oidc_debug(r, "enter");
103
104
  /* see if a userinfo endpoint is set (and not explicitly disabled with an empty value), otherwise there's
105
   * nothing to do for us */
106
416
  if ((oidc_cfg_provider_userinfo_endpoint_url_get(provider) == NULL) ||
107
416
      (_oidc_strcmp(oidc_cfg_provider_userinfo_endpoint_url_get(provider), "") == 0)) {
108
416
    oidc_debug(r, "not retrieving userinfo claims because userinfo_endpoint is not set or disabled");
109
416
    goto end;
110
416
  }
111
112
  /* see if there's an access token, otherwise we can't call the userinfo endpoint at all */
113
0
  if (access_token == NULL) {
114
0
    oidc_debug(r, "not retrieving userinfo claims because access_token is not provided");
115
0
    goto end;
116
0
  }
117
118
0
  if ((id_token_sub == NULL) && (session != NULL)) {
119
    // when refreshing claims from the userinfo endpoint
120
0
    id_token_claims = oidc_session_get_idtoken_claims(r, session);
121
0
    if (id_token_claims != NULL) {
122
0
      oidc_jose_get_string(r->pool, id_token_claims, OIDC_CLAIM_SUB, FALSE, &id_token_sub, NULL);
123
0
    } else {
124
0
      oidc_debug(r, "no id_token_claims found in session");
125
0
    }
126
0
  }
127
128
  /*
129
   * NB: userinfo results are ultimately stored in the session in their stringified form; a future
130
   * improvement would be to keep session storage as typed JSON to avoid the string<->JSON conversions
131
   */
132
133
  /* try to get claims from the userinfo endpoint using the provided access token */
134
0
  if (oidc_proto_userinfo_request(r, c, provider, id_token_sub, access_token, access_token_type, &result,
135
0
          userinfo_jwt, userinfo_claims, &response_code) == TRUE)
136
0
    goto end;
137
138
  /* see if this is the initial call to the user info endpoint upon receiving the authorization response */
139
0
  if (session == NULL) {
140
0
    oidc_error(r, "resolving user info claims with the provided access token failed, nothing will be "
141
0
            "stored in the session");
142
0
    result = NULL;
143
0
    goto end;
144
0
  }
145
146
  // a connectivity error rather than a HTTP error: refreshing the access token won't help
147
0
  if (response_code == 0) {
148
0
    oidc_error(r, "resolving user info claims failed with a connectivity error, no attempt will be made to "
149
0
            "refresh the access token and try again");
150
0
    result = NULL;
151
0
    goto end;
152
0
  }
153
154
  /* only an "invalid_token" response (401) indicates the access token may have expired and is worth
155
   * refreshing; any other HTTP error (e.g. 403, 5xx) will not be resolved by presenting a fresh token */
156
0
  if (response_code != HTTP_UNAUTHORIZED) {
157
0
    oidc_error(r,
158
0
         "resolving user info claims failed with HTTP status %ld; since this is not a 401 the access "
159
0
         "token will not be refreshed",
160
0
         response_code);
161
0
    result = NULL;
162
0
    goto end;
163
0
  }
164
165
  /* first call to user info endpoint failed, but this is for an existing session and the access token may have
166
   * just expired, so refresh it */
167
0
  if (oidc_refresh_token_grant(r, c, session, provider, &refreshed_access_token, &refreshed_access_token_type,
168
0
             NULL) == FALSE) {
169
0
    oidc_error(r, "refreshing access token failed, claims will not be retrieved/refreshed from the "
170
0
            "userinfo endpoint");
171
0
    result = NULL;
172
0
    goto end;
173
0
  }
174
175
  /* try again with the new access token */
176
0
  if (oidc_proto_userinfo_request(r, c, provider, id_token_sub, refreshed_access_token,
177
0
          refreshed_access_token_type, &result, userinfo_jwt, userinfo_claims,
178
0
          NULL) == FALSE) {
179
180
0
    oidc_error(r, "resolving user info claims with the refreshed access token failed, nothing will be "
181
0
            "stored in the session");
182
0
    result = NULL;
183
0
    goto end;
184
0
  }
185
186
416
end:
187
188
416
  oidc_debug(r, "return (%d)", result != NULL);
189
190
416
  return result;
191
0
}
192
193
/*
194
 * get (new) claims from the userinfo endpoint
195
 */
196
apr_byte_t oidc_userinfo_refresh_claims(request_rec *r, oidc_cfg_t *cfg, oidc_session_t *session,
197
0
          apr_byte_t *needs_save) {
198
199
0
  apr_byte_t rc = TRUE;
200
0
  oidc_provider_t *provider = NULL;
201
0
  const char *claims = NULL;
202
0
  char *userinfo_jwt = NULL;
203
0
  oidc_json_t *userinfo_claims = NULL;
204
0
  apr_time_t last_refresh = 0;
205
206
  /* see if we can do anything here, i.e. a refresh interval is configured */
207
0
  int interval = oidc_session_get_userinfo_refresh_interval(r, session);
208
209
0
  oidc_debug(r, "interval=%d", interval);
210
211
0
  if (interval <= -1)
212
0
    goto end;
213
214
  /* get the current provider info */
215
0
  if (oidc_get_provider_from_session(r, cfg, session, &provider) == FALSE) {
216
0
    *needs_save = TRUE;
217
0
    rc = FALSE;
218
0
    goto end;
219
0
  }
220
221
  /* nothing to do without a userinfo endpoint (or when it is explicitly disabled with an empty value) */
222
0
  if ((oidc_cfg_provider_userinfo_endpoint_url_get(provider) == NULL) ||
223
0
      (_oidc_strcmp(oidc_cfg_provider_userinfo_endpoint_url_get(provider), "") == 0))
224
0
    goto end;
225
226
  /* get the last refresh timestamp from the session info */
227
0
  last_refresh = oidc_session_get_userinfo_last_refresh(r, session);
228
229
0
  oidc_debug(r,
230
0
       "refresh needed in: %" APR_TIME_T_FMT " seconds (last_refresh=%" APR_TIME_T_FMT
231
0
       ", interval=%d, now=%" APR_TIME_T_FMT ")",
232
0
       apr_time_sec(last_refresh + apr_time_from_sec(interval) - apr_time_now()),
233
0
       apr_time_sec(last_refresh), interval, apr_time_sec(apr_time_now()));
234
235
  /* see if we need to refresh again */
236
0
  if (last_refresh + apr_time_from_sec(interval) >= apr_time_now())
237
0
    goto end;
238
239
  /* retrieve the current claims using the current access token */
240
0
  claims = oidc_userinfo_retrieve_claims(r, cfg, provider, oidc_session_get_access_token(r, session),
241
0
                 oidc_session_get_access_token_type(r, session), session, NULL,
242
0
                 &userinfo_claims, &userinfo_jwt);
243
244
  /* store claims resolved from userinfo endpoint */
245
0
  oidc_userinfo_store_claims(r, cfg, session, provider, userinfo_claims, userinfo_jwt);
246
247
0
  oidc_json_decref(userinfo_claims);
248
249
0
  if (claims == NULL) {
250
0
    *needs_save = FALSE;
251
0
    rc = FALSE;
252
0
  } else {
253
    /* indicated something changed */
254
0
    *needs_save = TRUE;
255
0
  }
256
257
0
end:
258
259
0
  oidc_debug(r, "return: %d", rc);
260
261
0
  return rc;
262
0
}
263
264
0
#define OIDC_USERINFO_SIGNED_JWT_EXP_DEFAULT 60
265
0
#define OIDC_USERINFO_SIGNED_JWT_CACHE_TTL_DEFAULT -1
266
0
#define OIDC_USERINFO_SIGNED_JWT_CACHE_TTL_ENVVAR "OIDC_USERINFO_SIGNED_JWT_CACHE_TTL"
267
268
/*
269
 * obtain the signed JWT cache TTL from the environment variables
270
 */
271
0
static int oidc_userinfo_signed_jwt_cache_ttl(const request_rec *r) {
272
0
  const char *s_ttl = apr_table_get(r->subprocess_env, OIDC_USERINFO_SIGNED_JWT_CACHE_TTL_ENVVAR);
273
0
  return _oidc_str_to_int(s_ttl, OIDC_USERINFO_SIGNED_JWT_CACHE_TTL_DEFAULT);
274
0
}
275
276
/*
277
 * create a signed JWT with s_claims payload and return the serialized form in cser
278
 */
279
static apr_byte_t oidc_userinfo_create_signed_jwt(request_rec *r, oidc_cfg_t *cfg, const oidc_session_t *session,
280
0
              oidc_json_t *claims, char **cser) {
281
0
  apr_byte_t rv = FALSE;
282
0
  oidc_jwt_t *jwt = NULL;
283
0
  oidc_jwk_t *jwk = NULL;
284
0
  oidc_jose_error_t err;
285
0
  apr_time_t access_token_expires = -1;
286
0
  char *key = NULL;
287
0
  int ttl = 0;
288
0
  int exp = 0;
289
0
  apr_time_t expiry = 0;
290
291
0
  if (oidc_proto_jwt_create_from_first_pkey(r, cfg, &jwk, &jwt, FALSE) == FALSE)
292
0
    goto end;
293
294
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_AUD,
295
0
         oidc_json_string(oidc_util_url_cur(r, oidc_cfg_x_forwarded_headers_get(cfg))));
296
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_ISS,
297
0
         oidc_json_string(oidc_cfg_provider_issuer_get(oidc_cfg_provider_get(cfg))));
298
299
0
  if (oidc_json_merge(r, claims, jwt->payload.value.json) == FALSE)
300
0
    goto end;
301
0
  const char *s_claims =
302
0
      oidc_json_encode(r->pool, jwt->payload.value.json, OIDC_JSON_PRESERVE_ORDER | OIDC_JSON_COMPACT);
303
0
  if (oidc_jose_hash_and_base64url_encode(r->pool, OIDC_JOSE_ALG_SHA256, s_claims,
304
0
            (int)_oidc_strlen(s_claims) + 1, &key, &err) == FALSE) {
305
0
    oidc_error(r, "oidc_jose_hash_and_base64url_encode failed: %s", oidc_jose_e2s(r->pool, err));
306
0
    goto end;
307
0
  }
308
309
0
  ttl = oidc_userinfo_signed_jwt_cache_ttl(r);
310
0
  if (ttl > -1)
311
0
    oidc_cache_get_signed_jwt(r, key, cser);
312
313
0
  if (*cser != NULL) {
314
0
    oidc_debug(r, "signed JWT found in cache");
315
0
    rv = TRUE;
316
0
    goto end;
317
0
  }
318
319
0
  if (oidc_json_object_get(jwt->payload.value.json, OIDC_CLAIM_JTI) == NULL) {
320
0
    oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_JTI,
321
0
           oidc_json_string(oidc_proto_jti_gen(r)));
322
0
  }
323
0
  if (oidc_json_object_get(jwt->payload.value.json, OIDC_CLAIM_IAT) == NULL) {
324
0
    oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_IAT,
325
0
           oidc_json_integer(apr_time_sec(apr_time_now())));
326
0
  }
327
0
  if (oidc_json_object_get(jwt->payload.value.json, OIDC_CLAIM_EXP) == NULL) {
328
0
    access_token_expires = oidc_session_get_access_token_expires(r, session);
329
0
    oidc_json_object_set_new(
330
0
        jwt->payload.value.json, OIDC_CLAIM_EXP,
331
0
        oidc_json_integer(access_token_expires > 0
332
0
            ? apr_time_sec(access_token_expires)
333
0
            : apr_time_sec(apr_time_now()) + OIDC_USERINFO_SIGNED_JWT_EXP_DEFAULT));
334
0
  }
335
336
0
  if (oidc_proto_jwt_sign_and_serialize(r, jwk, jwt, cser) == FALSE)
337
0
    goto end;
338
339
0
  rv = TRUE;
340
341
0
  if (ttl < 0)
342
0
    goto end;
343
344
0
  if (ttl == 0) {
345
    // need to get the cache ttl from the exp claim
346
0
    oidc_json_object_get_int(jwt->payload.value.json, OIDC_CLAIM_EXP, &exp, 0);
347
    // actually the exp claim always exists by now
348
0
    expiry = (exp > 0) ? apr_time_from_sec(exp)
349
0
           : apr_time_now() + apr_time_from_sec(OIDC_USERINFO_SIGNED_JWT_EXP_DEFAULT);
350
0
  } else {
351
    // ttl > 0
352
0
    expiry = apr_time_now() + apr_time_from_sec(ttl);
353
0
  }
354
355
0
  oidc_debug(r, "caching signed JWT with ~ttl(%ld)", apr_time_sec(expiry - apr_time_now()));
356
0
  oidc_cache_set_signed_jwt(r, key, *cser, expiry);
357
358
0
end:
359
360
0
  if (jwt)
361
0
    oidc_jwt_destroy(jwt);
362
363
0
  return rv;
364
0
}
365
366
/*
367
 * set an appinfo value, falling back to default_name (with the default
368
 * header prefix) when no explicit name was configured
369
 */
370
static void oidc_userinfo_appinfo_set(request_rec *r, const char *name, const char *default_name, const char *value,
371
0
              oidc_appinfo_pass_in_t pass_in, oidc_appinfo_encoding_t encoding) {
372
0
  oidc_util_appinfo_set(r, name ? name : default_name, value, name ? "" : OIDC_DEFAULT_HEADER_PREFIX, pass_in,
373
0
            encoding);
374
0
}
375
376
/*
377
 * pass the userinfo JWT stored in the session to the app, rejecting the
378
 * client-cookie session type which does not support storing such JWTs
379
 */
380
static void oidc_userinfo_pass_as_jwt(request_rec *r, const oidc_cfg_t *cfg, const oidc_session_t *session,
381
              const char *name, oidc_appinfo_pass_in_t pass_in,
382
0
              oidc_appinfo_encoding_t encoding) {
383
384
0
  if (oidc_cfg_session_type_get(cfg) == OIDC_SESSION_TYPE_CLIENT_COOKIE) {
385
0
    oidc_error(r, "session type \"client-cookie\" does not allow storing/passing a "
386
0
            "userinfo JWT; use \"" OIDCSessionType " server-cache\" for that");
387
0
    return;
388
0
  }
389
390
0
  const char *s_userinfo_jwt = oidc_session_get_userinfo_jwt(r, session);
391
0
  if (s_userinfo_jwt == NULL) {
392
0
    oidc_debug(r, "configured to pass userinfo in a JWT, but no such JWT was found in the session "
393
0
            "(probably no such JWT was returned from the userinfo endpoint)");
394
0
    return;
395
0
  }
396
397
0
  oidc_userinfo_appinfo_set(r, name, OIDC_APP_INFO_USERINFO_JWT, s_userinfo_jwt, pass_in, encoding);
398
0
}
399
400
/*
401
 * dispatch a single configured "pass userinfo as" entry to the matching
402
 * encoding-specific handler
403
 */
404
static void oidc_userinfo_pass_entry(request_rec *r, oidc_cfg_t *cfg, const oidc_session_t *session,
405
             oidc_json_t *claims, const oidc_pass_user_info_as_t *p,
406
0
             oidc_appinfo_pass_in_t pass_in, oidc_appinfo_encoding_t encoding) {
407
408
0
  char *cser = NULL;
409
410
0
  switch (p->type) {
411
0
  case OIDC_PASS_USERINFO_AS_CLAIMS:
412
    /* set the userinfo claims in the app headers */
413
0
    oidc_set_app_claims(r, cfg, claims);
414
0
    break;
415
0
  case OIDC_PASS_USERINFO_AS_JSON_OBJECT:
416
    /* pass the userinfo JSON object to the app in a header or environment variable */
417
0
    oidc_userinfo_appinfo_set(
418
0
        r, p->name, OIDC_APP_INFO_USERINFO_JSON,
419
0
        oidc_json_encode(r->pool, claims, OIDC_JSON_PRESERVE_ORDER | OIDC_JSON_COMPACT), pass_in, encoding);
420
0
    break;
421
0
  case OIDC_PASS_USERINFO_AS_JWT:
422
0
    oidc_userinfo_pass_as_jwt(r, cfg, session, p->name, pass_in, encoding);
423
0
    break;
424
0
  case OIDC_PASS_USERINFO_AS_SIGNED_JWT:
425
0
    if (oidc_userinfo_create_signed_jwt(r, cfg, session, claims, &cser) == TRUE)
426
0
      oidc_userinfo_appinfo_set(r, p->name, OIDC_APP_INFO_SIGNED_JWT, cser, pass_in, encoding);
427
0
    break;
428
0
  default:
429
0
    break;
430
0
  }
431
0
}
432
433
/*
434
 * resolve the claims to pass to the app, optionally applying a JQ filter
435
 * to the userinfo claims stored in the session; *filtered receives the
436
 * newly allocated oidc_json_t that the caller must release, or NULL when no
437
 * filter was applied
438
 */
439
static oidc_json_t *oidc_userinfo_resolve_claims(request_rec *r, const oidc_session_t *session,
440
0
             oidc_json_t **filtered) {
441
442
0
  *filtered = NULL;
443
444
#ifdef USE_LIBJQ
445
  const char *s_filter = oidc_cfg_dir_userinfo_claims_expr_get(r);
446
  if (s_filter != NULL) {
447
    const char *s_claims = oidc_util_jq_filter(r, oidc_session_get_userinfo_claims(r, session), s_filter);
448
    if (oidc_json_decode_object(r, s_claims, filtered) == FALSE) {
449
      oidc_error(r, "JQ filtering of claims for [%s] resulted in invalid JSON object, filter='%s'",
450
           "userinfo", s_filter);
451
      return NULL;
452
    }
453
    return *filtered;
454
  }
455
#endif
456
457
0
  return oidc_session_get_userinfo_claims(r, session);
458
0
}
459
460
/*
461
 * pass the userinfo claims to headers and/or environment variables, encoded as configured
462
 */
463
void oidc_userinfo_pass_as(request_rec *r, oidc_cfg_t *cfg, const oidc_session_t *session,
464
0
         oidc_appinfo_pass_in_t pass_in, oidc_appinfo_encoding_t encoding) {
465
466
0
  const apr_array_header_t *pass_userinfo_as = oidc_cfg_dir_pass_userinfo_as_get(r);
467
0
  oidc_json_t *filtered_claims = NULL;
468
0
  oidc_json_t *claims = oidc_userinfo_resolve_claims(r, session, &filtered_claims);
469
470
0
  if (claims == NULL)
471
0
    return;
472
473
0
  for (int i = 0; (pass_userinfo_as != NULL) && (i < pass_userinfo_as->nelts); i++)
474
0
    oidc_userinfo_pass_entry(r, cfg, session, claims,
475
0
           APR_ARRAY_IDX(pass_userinfo_as, i, oidc_pass_user_info_as_t *), pass_in,
476
0
           encoding);
477
478
0
  if (filtered_claims)
479
0
    oidc_json_decref(filtered_claims);
480
0
}