Coverage Report

Created: 2026-11-12 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mod_auth_openidc/src/proto/auth.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/parse.h"
44
#include "mod_auth_openidc.h"
45
#include "proto/proto.h"
46
#include "util/util.h"
47
48
/*
49
 * setup for an endpoint call without authentication
50
 */
51
0
static apr_byte_t oidc_proto_endpoint_auth_none(const char *client_id, apr_table_t *params) {
52
0
  apr_table_set(params, OIDC_PROTO_CLIENT_ID, client_id);
53
0
  return TRUE;
54
0
}
55
56
/*
57
 * setup for an endpoint call with OIDC client_secret_basic authentication
58
 */
59
static apr_byte_t oidc_proto_endpoint_client_secret_basic(request_rec *r, const char *client_id,
60
246
                const char *client_secret, char **basic_auth_str) {
61
246
  oidc_debug(r, "enter");
62
246
  if (client_secret == NULL) {
63
0
    oidc_error(r, "no client secret is configured");
64
0
    return FALSE;
65
0
  }
66
246
  *basic_auth_str =
67
246
      apr_psprintf(r->pool, "%s:%s", oidc_http_url_encode(r, client_id), oidc_http_url_encode(r, client_secret));
68
69
246
  return TRUE;
70
246
}
71
72
/*
73
 * setup for an endpoint call with OIDC client_secret_post authentication
74
 */
75
static apr_byte_t oidc_proto_endpoint_client_secret_post(request_rec *r, const char *client_id,
76
0
               const char *client_secret, apr_table_t *params) {
77
0
  oidc_debug(r, "enter");
78
0
  if (client_secret == NULL) {
79
0
    oidc_error(r, "no client secret is configured");
80
0
    return FALSE;
81
0
  }
82
0
  apr_table_set(params, OIDC_PROTO_CLIENT_ID, client_id);
83
0
  apr_table_set(params, OIDC_PROTO_CLIENT_SECRET, client_secret);
84
0
  return TRUE;
85
0
}
86
87
/*
88
 * helper function to create a JWT assertion for endpoint authentication
89
 */
90
static apr_byte_t oidc_proto_jwt_create(request_rec *r, const char *client_id, oidc_proto_endpoint_auth_aud_t audience,
91
0
          oidc_jwt_t **out) {
92
93
0
  *out = oidc_jwt_new(r->pool, TRUE, TRUE);
94
0
  oidc_jwt_t *jwt = *out;
95
96
  /* RFC 10070 section 4: type the assertion explicitly, but only when its audience is the issuer identifier;
97
   * the "typ" signals that the client follows the tightened audience requirements, so a pre-RFC 10070
98
   * audience such as the token endpoint URL must not carry it */
99
0
  if (audience.is_issuer)
100
0
    oidc_json_object_set_new(jwt->header.value.json, OIDC_CLAIM_TYP,
101
0
           oidc_json_string(OIDC_PROTO_CLIENT_ASSERTION_JWT_TYP));
102
103
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_ISS, oidc_json_string(client_id));
104
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_SUB, oidc_json_string(client_id));
105
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_AUD, oidc_json_string(audience.value));
106
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_JTI, oidc_json_string(oidc_proto_jti_gen(r)));
107
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_EXP,
108
0
         oidc_json_integer(apr_time_sec(apr_time_now()) + 60));
109
0
  oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_IAT,
110
0
         oidc_json_integer(apr_time_sec(apr_time_now())));
111
112
0
  return TRUE;
113
0
}
114
115
/*
116
 * helper function to add a JWT assertion to the HTTP request as endpoint authentication
117
 */
118
static apr_byte_t oidc_proto_jwt_sign_and_add(request_rec *r, apr_table_t *params, oidc_jwt_t *jwt,
119
0
                const oidc_jwk_t *jwk) {
120
0
  char *cser = NULL;
121
122
0
  if (oidc_proto_jwt_sign_and_serialize(r, jwk, jwt, &cser) == FALSE)
123
0
    return FALSE;
124
125
0
  apr_table_setn(params, OIDC_PROTO_CLIENT_ASSERTION_TYPE, OIDC_PROTO_CLIENT_ASSERTION_TYPE_JWT_BEARER);
126
0
  apr_table_set(params, OIDC_PROTO_CLIENT_ASSERTION, cser);
127
128
0
  return TRUE;
129
0
}
130
131
0
#define OIDC_PROTO_JWT_ASSERTION_SYMMETRIC_ALG OIDC_JOSE_HDR_ALG_HS256
132
133
/*
134
 * create a JWT assertion signed with the client secret and add it to the HTTP request as endpoint authentication
135
 */
136
static apr_byte_t oidc_proto_endpoint_auth_client_secret_jwt(request_rec *r, const char *client_id,
137
                   const char *client_secret,
138
                   oidc_proto_endpoint_auth_aud_t audience,
139
0
                   apr_table_t *params) {
140
0
  oidc_jwt_t *jwt = NULL;
141
0
  oidc_jose_error_t err;
142
143
0
  oidc_debug(r, "enter");
144
145
0
  if (oidc_proto_jwt_create(r, client_id, audience, &jwt) == FALSE)
146
0
    return FALSE;
147
148
0
  oidc_jwk_t *jwk = oidc_jwk_create_symmetric_key(r->pool, NULL, (const unsigned char *)client_secret,
149
0
              (unsigned int)_oidc_strlen(client_secret), FALSE, &err);
150
0
  if (jwk == NULL) {
151
0
    oidc_error(r, "parsing of client secret into JWK failed: %s", oidc_jose_e2s(r->pool, err));
152
0
    oidc_jwt_destroy(jwt);
153
0
    return FALSE;
154
0
  }
155
156
0
  jwt->header.alg = apr_pstrdup(r->pool, OIDC_PROTO_JWT_ASSERTION_SYMMETRIC_ALG);
157
158
0
  oidc_proto_jwt_sign_and_add(r, params, jwt, jwk);
159
160
0
  oidc_jwt_destroy(jwt);
161
0
  oidc_jwk_destroy(jwk);
162
163
0
  return TRUE;
164
0
}
165
166
/*
167
 * helper function that returns the bearer access token as the endpoint authentication method if configured
168
 */
169
static apr_byte_t oidc_proto_endpoint_access_token_bearer(request_rec *r, oidc_cfg_t *cfg,
170
0
                const char *bearer_access_token, char **bearer_auth_str) {
171
172
0
  apr_byte_t rv = TRUE;
173
174
0
  if (bearer_access_token != NULL) {
175
0
    *bearer_auth_str = apr_psprintf(r->pool, "%s", bearer_access_token);
176
0
  } else {
177
0
    oidc_error(r, "endpoint auth method set to bearer access token but no token is provided");
178
0
    rv = FALSE;
179
0
  }
180
181
0
  return rv;
182
0
}
183
184
/*
185
 * pick the private signing key (and its accompanying x5t when available) used to sign a private_key_jwt
186
 * assertion: client-specific keys take precedence over the module-wide configured private keys
187
 */
188
oidc_jwk_t *oidc_proto_endpoint_auth_pick_signing_key(const oidc_cfg_t *cfg, const apr_array_header_t *client_keys,
189
0
                  const char **x5t) {
190
0
  const oidc_jwk_t *jwk_pub = NULL;
191
0
  oidc_jwk_t *jwk = NULL;
192
193
0
  *x5t = NULL;
194
0
  if ((client_keys != NULL) && (client_keys->nelts > 0)) {
195
0
    jwk = oidc_util_key_list_first(client_keys, -1, OIDC_JOSE_JWK_SIG_STR);
196
0
    if (jwk && jwk->x5t)
197
0
      *x5t = jwk->x5t;
198
0
    return jwk;
199
0
  }
200
0
  if ((oidc_cfg_private_keys_get(cfg) != NULL) && (oidc_cfg_private_keys_get(cfg)->nelts > 0)) {
201
0
    jwk = oidc_util_key_list_first(oidc_cfg_private_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR);
202
0
    jwk_pub = oidc_util_key_list_first(oidc_cfg_public_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR);
203
0
    if (jwk_pub && jwk_pub->x5t)
204
      // populate x5t; at least required for Microsoft Entra ID / Azure AD
205
0
      *x5t = jwk_pub->x5t;
206
0
  }
207
0
  return jwk;
208
0
}
209
210
/*
211
 * create a JWT assertion signed with the configured private key and add it to the HTTP request as endpoint
212
 * authentication
213
 */
214
static apr_byte_t oidc_proto_endpoint_auth_private_key_jwt(request_rec *r, const oidc_cfg_t *cfg,
215
                 const char *token_endpoint_auth_alg, const char *client_id,
216
                 const apr_array_header_t *client_keys,
217
                 oidc_proto_endpoint_auth_aud_t audience,
218
0
                 apr_table_t *params) {
219
0
  apr_byte_t rv = FALSE;
220
0
  oidc_jwt_t *jwt = NULL;
221
0
  const oidc_jwk_t *jwk = NULL;
222
0
  const char *x5t = NULL;
223
0
  const char *alg = NULL;
224
225
0
  oidc_debug(r, "enter");
226
227
0
  if (oidc_proto_jwt_create(r, client_id, audience, &jwt) == FALSE)
228
0
    return FALSE;
229
230
0
  jwk = oidc_proto_endpoint_auth_pick_signing_key(cfg, client_keys, &x5t);
231
0
  if (jwk == NULL) {
232
0
    oidc_error(r, "no private signing keys have been configured to use for private_key_jwt client "
233
0
            "authentication (" OIDCPrivateKeyFiles ")");
234
0
    goto end;
235
0
  }
236
237
0
  if (x5t != NULL)
238
0
    jwt->header.x5t = apr_pstrdup(r->pool, x5t);
239
0
  jwt->header.kid = apr_pstrdup(r->pool, jwk->kid);
240
241
0
  if (token_endpoint_auth_alg != NULL) {
242
    // oidc_proto_jwt_sign_and_add will fail later if no corresponding key was configured
243
0
    alg = token_endpoint_auth_alg;
244
0
  } else {
245
0
    alg = oidc_jwk_default_jws_alg(jwk);
246
0
    if (alg == NULL) {
247
0
      oidc_error(
248
0
          r, "no valid signing key (RSA or Elliptic Curve) could be found in " OIDCPrivateKeyFiles);
249
0
      goto end;
250
0
    }
251
0
  }
252
0
  jwt->header.alg = apr_pstrdup(r->pool, alg);
253
254
0
  oidc_debug(r, "signing private_key_jwt assertion with algorithm: %s", jwt->header.alg);
255
256
  // this fails if the wrong type of key  configured for an explicitly specified algorithm
257
0
  rv = oidc_proto_jwt_sign_and_add(r, params, jwt, jwk);
258
259
0
end:
260
261
0
  if (jwt)
262
0
    oidc_jwt_destroy(jwt);
263
264
0
  return rv;
265
0
}
266
267
/*
268
 * Keep this idempotent: retries rebuild the authentication data to obtain a fresh client
269
 * assertion, so credentials must replace earlier values rather than create duplicates.
270
 */
271
apr_byte_t oidc_proto_token_endpoint_auth(request_rec *r, oidc_cfg_t *cfg, const char *token_endpoint_auth,
272
            const char *token_endpoint_auth_alg, const char *client_id,
273
            const char *client_secret, const apr_array_header_t *client_keys,
274
            oidc_proto_endpoint_auth_aud_t audience, apr_table_t *params,
275
            const char *bearer_access_token, char **basic_auth_str,
276
246
            char **bearer_auth_str) {
277
278
246
  oidc_debug(r, "token_endpoint_auth=%s", token_endpoint_auth);
279
280
246
  if (client_id == NULL) {
281
0
    oidc_debug(r, "no client ID set: assume we don't need to authenticate");
282
0
    return TRUE;
283
0
  }
284
285
  // default is client_secret_basic, but only if a client_secret is set,
286
  // otherwise we are a public client
287
246
  if ((token_endpoint_auth == NULL) && (client_secret != NULL))
288
246
    token_endpoint_auth = OIDC_PROTO_CLIENT_SECRET_BASIC;
289
290
246
  if ((token_endpoint_auth == NULL) || (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_ENDPOINT_AUTH_NONE) == 0)) {
291
0
    oidc_debug(
292
0
        r,
293
0
        "no client secret is configured or the token endpoint auth method was set to \"%s\"; calling the "
294
0
        "token endpoint without client authentication; only public clients are supported",
295
0
        OIDC_PROTO_ENDPOINT_AUTH_NONE);
296
0
    return oidc_proto_endpoint_auth_none(client_id, params);
297
0
  }
298
299
  // RFC 8705: the client authenticates at the TLS layer with its mutual-TLS client
300
  // certificate; only the client_id is passed in the request body
301
246
  if (oidc_cfg_endpoint_auth_is_mtls(token_endpoint_auth)) {
302
0
    oidc_debug(r,
303
0
         "\"%s\": authentication occurs at the TLS layer, adding only the client_id to the request",
304
0
         token_endpoint_auth);
305
0
    return oidc_proto_endpoint_auth_none(client_id, params);
306
0
  }
307
308
  // if no client_secret is set and we don't authenticate using private_key_jwt,
309
  // we can only be a public client since the other methods require a client_secret
310
246
  if ((client_secret == NULL) && (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_PRIVATE_KEY_JWT) != 0)) {
311
0
    oidc_debug(r, "no client secret set and not using private_key_jwt, assume we are a public client");
312
0
    return oidc_proto_endpoint_auth_none(client_id, params);
313
0
  }
314
315
246
  if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_BASIC) == 0)
316
246
    return oidc_proto_endpoint_client_secret_basic(r, client_id, client_secret, basic_auth_str);
317
318
0
  if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_POST) == 0)
319
0
    return oidc_proto_endpoint_client_secret_post(r, client_id, client_secret, params);
320
321
0
  if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_JWT) == 0)
322
0
    return oidc_proto_endpoint_auth_client_secret_jwt(r, client_id, client_secret, audience, params);
323
324
0
  if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_PRIVATE_KEY_JWT) == 0)
325
0
    return oidc_proto_endpoint_auth_private_key_jwt(r, cfg, token_endpoint_auth_alg, client_id, client_keys,
326
0
                audience, params);
327
328
0
  if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_BEARER_ACCESS_TOKEN) == 0) {
329
0
    return oidc_proto_endpoint_access_token_bearer(r, cfg, bearer_access_token, bearer_auth_str);
330
0
  }
331
332
0
  oidc_error(r, "uhm, shouldn't be here...");
333
334
0
  return FALSE;
335
0
}