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/proto/jwks.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 "metadata.h"
44
#include "proto/proto.h"
45
#include "util/util.h"
46
47
/*
48
 * when no kid/x5t was specified, include the JWK in the result if it is usable for signing;
49
 * takes ownership of jwk (either inserts it into result or destroys it)
50
 */
51
static void oidc_proto_jwks_key_include_any(request_rec *r, oidc_jwk_t *jwk, const oidc_json_t *elem,
52
0
              apr_hash_t *result) {
53
0
  const char *use = oidc_json_string_value(oidc_json_object_get(elem, OIDC_JOSE_JWK_USE_STR));
54
0
  if ((use != NULL) && (_oidc_strcmp(use, OIDC_JOSE_JWK_SIG_STR) != 0)) {
55
0
    oidc_debug(r, "skipping key because of non-matching \"%s\": \"%s\"", OIDC_JOSE_JWK_USE_STR, use);
56
0
    oidc_jwk_destroy(jwk);
57
0
    return;
58
0
  }
59
60
0
  char *jwk_json = NULL;
61
0
  oidc_jose_error_t err;
62
0
  oidc_jwk_to_json(r->pool, jwk, &jwk_json, &err);
63
0
  oidc_debug(r, "no kid/x5t to match, include matching key type: %s", jwk_json);
64
0
  if (jwk->kid != NULL)
65
0
    apr_hash_set(result, jwk->kid, APR_HASH_KEY_STRING, jwk);
66
0
  else
67
    // can do this because we never remove anything from the list
68
0
    apr_hash_set(result, apr_psprintf(r->pool, "%d", apr_hash_count(result)), APR_HASH_KEY_STRING, jwk);
69
0
}
70
71
/*
72
 * try a single JWKS entry against the JWT header;
73
 * returns TRUE when a specific kid/x5t match was found so the caller can stop iterating
74
 */
75
static apr_byte_t oidc_proto_jwks_key_apply(request_rec *r, oidc_jwt_t *jwt, const oidc_json_t *elem, const char *x5t,
76
0
              apr_hash_t *result) {
77
0
  oidc_jwk_t *jwk = NULL;
78
0
  oidc_jose_error_t err;
79
0
  char *jwk_json = NULL;
80
0
  char *s_x5t = NULL;
81
82
0
  if (oidc_jwk_parse_json(r->pool, elem, &jwk, &err) == FALSE) {
83
0
    oidc_warn(r, "oidc_jwk_parse_json failed: %s", oidc_jose_e2s(r->pool, err));
84
0
    return FALSE;
85
0
  }
86
87
  /* skip keys whose type does not match the JWT algorithm */
88
0
  if (oidc_jwt_alg2kty(jwt) != jwk->kty) {
89
0
    oidc_debug(r,
90
0
         "skipping non matching kty=%d for kid=%s because it doesn't match requested kty=%d, kid=%s",
91
0
         jwk->kty, jwk->kid, oidc_jwt_alg2kty(jwt), jwt->header.kid);
92
0
    oidc_jwk_destroy(jwk);
93
0
    return FALSE;
94
0
  }
95
96
  /* no specific kid/x5t requested: include any sig-usable key with a matching type */
97
0
  if ((jwt->header.kid == NULL) && (x5t == NULL)) {
98
0
    oidc_proto_jwks_key_include_any(r, jwk, elem, result);
99
0
    return FALSE;
100
0
  }
101
102
  /* compare the requested kid against the current element */
103
0
  if ((jwt->header.kid != NULL) && (jwk->kid != NULL) && (_oidc_strcmp(jwt->header.kid, jwk->kid) == 0)) {
104
0
    oidc_jwk_to_json(r->pool, jwk, &jwk_json, &err);
105
0
    oidc_debug(r, "found matching kid: \"%s\" for jwk: %s", jwt->header.kid, jwk_json);
106
0
    apr_hash_set(result, jwt->header.kid, APR_HASH_KEY_STRING, jwk);
107
0
    return TRUE;
108
0
  }
109
110
  /* compare the requested thumbprint against the current element */
111
0
  oidc_json_object_get_string(r->pool, elem, OIDC_JOSE_JWK_X5T_STR, &s_x5t, NULL);
112
0
  if ((s_x5t != NULL) && (x5t != NULL) && (_oidc_strcmp(x5t, s_x5t) == 0)) {
113
0
    oidc_jwk_to_json(r->pool, jwk, &jwk_json, &err);
114
0
    oidc_debug(r, "found matching %s: \"%s\" for jwk: %s", OIDC_JOSE_JWK_X5T_STR, x5t, jwk_json);
115
0
    apr_hash_set(result, x5t, APR_HASH_KEY_STRING, jwk);
116
0
    return TRUE;
117
0
  }
118
119
  /* the right key type but no matching kid/x5t */
120
0
  oidc_jwk_destroy(jwk);
121
0
  return FALSE;
122
0
}
123
124
/*
125
 * get the key from the JWKs that corresponds with the key specified in the header
126
 */
127
static apr_byte_t oidc_proto_jwks_key_get(request_rec *r, oidc_jwt_t *jwt, const oidc_json_t *j_jwks,
128
0
            apr_hash_t *result) {
129
130
  /* get the (optional) thumbprint for comparison */
131
0
  const char *x5t = oidc_jwt_hdr_get(jwt, OIDC_JOSE_JWK_X5T_STR);
132
0
  oidc_debug(r, "search for kid \"%s\" or thumbprint x5t \"%s\"", jwt->header.kid, x5t);
133
134
  /* get the "keys" JSON array from the JWKs object */
135
0
  const oidc_json_t *keys = oidc_json_object_get(j_jwks, OIDC_JOSE_JWKS_KEYS_STR);
136
0
  if ((keys == NULL) || !(oidc_json_is_array(keys))) {
137
0
    oidc_error(r, "\"%s\" array element is not a JSON array", OIDC_JOSE_JWKS_KEYS_STR);
138
0
    return FALSE;
139
0
  }
140
141
0
  for (size_t i = 0; i < oidc_json_array_size(keys); i++) {
142
0
    if (oidc_proto_jwks_key_apply(r, jwt, oidc_json_array_get(keys, i), x5t, result) == TRUE)
143
0
      break;
144
0
  }
145
146
0
  return TRUE;
147
0
}
148
149
/*
150
 * get the keys from the (possibly cached) set of JWKs on the jwk_uri that corresponds with the key specified in the
151
 * header
152
 */
153
apr_byte_t oidc_proto_jwks_uri_keys(request_rec *r, oidc_cfg_t *cfg, oidc_jwt_t *jwt, const oidc_jwks_uri_t *jwks_uri,
154
0
            int ssl_validate_server, apr_hash_t *keys, apr_byte_t *force_refresh) {
155
156
0
  oidc_json_t *j_jwks = NULL;
157
158
  /* get the set of JSON Web Keys for this provider (possibly by downloading them from the specified
159
   * provider->jwk_uri) */
160
0
  oidc_metadata_jwks_get(r, cfg, jwks_uri, ssl_validate_server, &j_jwks, force_refresh);
161
0
  if (j_jwks == NULL) {
162
0
    oidc_error(r, "could not %s JSON Web Keys", *force_refresh ? "refresh" : "get");
163
0
    return FALSE;
164
0
  }
165
166
  /* Select by kid, or all keys when absent. Errors follow the same refresh path as a miss. */
167
0
  oidc_proto_jwks_key_get(r, jwt, j_jwks, keys);
168
169
  /* no need anymore for the parsed oidc_json_t contents, release the it */
170
0
  oidc_json_decref(j_jwks);
171
172
  /* if we've got no keys and we did not do a fresh download, then the cache may be stale */
173
0
  if ((apr_hash_count(keys) < 1) && (*force_refresh == FALSE)) {
174
175
    /* we did not get a key, but we have not refreshed the JWKs from the jwks_uri yet */
176
0
    oidc_warn(r, "could not find a key in the cached JSON Web Keys, doing a forced refresh in case keys "
177
0
           "were rolled over");
178
    /* get the set of JSON Web Keys forcing a fresh download from the specified JWKs URI */
179
0
    *force_refresh = TRUE;
180
0
    return oidc_proto_jwks_uri_keys(r, cfg, jwt, jwks_uri, ssl_validate_server, keys, force_refresh);
181
0
  }
182
183
0
  oidc_debug(r, "returning %d key(s) obtained from the (possibly cached) JWKs URI", apr_hash_count(keys));
184
185
0
  return TRUE;
186
0
}