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/token.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 <limits.h>
44
45
#include "cfg/dir.h"
46
#include "metrics.h"
47
#include "proto/proto.h"
48
#include "util/util.h"
49
50
/*
51
 * check that the access_token type is supported
52
 */
53
static apr_byte_t oidc_proto_validate_token_type(request_rec *r, const oidc_provider_t *provider,
54
0
             const char *token_type) {
55
  /*  we only support bearer/Bearer and DPoP/dpop */
56
0
  if ((token_type != NULL) && (_oidc_strnatcasecmp(token_type, OIDC_PROTO_BEARER) != 0) &&
57
0
      (_oidc_strnatcasecmp(token_type, OIDC_PROTO_DPOP) != 0) &&
58
0
      (oidc_cfg_provider_userinfo_endpoint_url_get(provider) != NULL) &&
59
0
      (_oidc_strcmp(oidc_cfg_provider_userinfo_endpoint_url_get(provider), "") != 0)) {
60
0
    oidc_error(r,
61
0
         "token_type is \"%s\" and UserInfo endpoint (%s) for issuer \"%s\" is set: can only deal "
62
0
         "with \"%s\" or \"%s\" authentication against a UserInfo endpoint!",
63
0
         token_type, oidc_cfg_provider_userinfo_endpoint_url_get(provider),
64
0
         oidc_cfg_provider_issuer_get(provider), OIDC_PROTO_BEARER, OIDC_PROTO_DPOP);
65
0
    return FALSE;
66
0
  }
67
68
0
  return TRUE;
69
0
}
70
71
/*
72
 * send the request to the token endpoint
73
 */
74
static apr_byte_t oidc_proto_token_endpoint_call(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
75
             const apr_table_t *params, const char *basic_auth,
76
             const char *bearer_auth, const char *dpop, char **response,
77
258
             apr_hash_t *response_hdrs) {
78
79
258
  OIDC_METRICS_TIMING_START(r, cfg);
80
81
258
  if (oidc_http_post_form(r, oidc_cfg_provider_token_endpoint_url_get(provider), params, basic_auth, bearer_auth,
82
258
        dpop, oidc_cfg_provider_ssl_validate_server_get(provider), response, NULL,
83
258
        response_hdrs, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg),
84
258
        oidc_cfg_dir_pass_cookies_get(r),
85
258
        oidc_cfg_provider_token_endpoint_tls_client_cert_get(provider),
86
258
        oidc_cfg_provider_token_endpoint_tls_client_key_get(provider),
87
258
        oidc_cfg_provider_token_endpoint_tls_client_key_pwd_get(provider)) == FALSE) {
88
258
    oidc_error(r, "error when calling the token endpoint (%s)",
89
258
         oidc_cfg_provider_token_endpoint_url_get(provider));
90
258
    return FALSE;
91
258
  }
92
93
0
  OIDC_METRICS_TIMING_ADD(r, cfg, OM_PROVIDER_TOKEN);
94
95
0
  return TRUE;
96
258
}
97
98
/*
99
 * set up the DPoP request header and response header tracking for the initial token endpoint call
100
 */
101
static apr_byte_t oidc_proto_token_endpoint_dpop_prepare(request_rec *r, const oidc_cfg_t *cfg,
102
               const oidc_provider_t *provider, apr_hash_t **response_hdrs,
103
258
               char **dpop) {
104
105
258
  if (oidc_proto_profile_dpop_mode_get(provider) == OIDC_DPOP_MODE_OFF)
106
258
    return TRUE;
107
108
0
  *response_hdrs = apr_hash_make(r->pool);
109
0
  apr_hash_set(*response_hdrs, OIDC_HTTP_HDR_AUTHORIZATION, APR_HASH_KEY_STRING, "");
110
0
  apr_hash_set(*response_hdrs, OIDC_HTTP_HDR_DPOP_NONCE, APR_HASH_KEY_STRING, "");
111
0
  apr_hash_set(*response_hdrs, OIDC_HTTP_HDR_CONTENT_TYPE, APR_HASH_KEY_STRING, "");
112
113
0
  if ((oidc_proto_dpop_create(r, cfg, oidc_cfg_provider_token_endpoint_url_get(provider), "POST", NULL, NULL,
114
0
            dpop) == FALSE) &&
115
0
      (oidc_proto_profile_dpop_mode_get(provider) == OIDC_DPOP_MODE_REQUIRED))
116
0
    return FALSE;
117
118
0
  return TRUE;
119
0
}
120
121
/*
122
 * add the configured token endpoint client authentication to the request; may be called more than
123
 * once for the same params table (see the DPoP nonce retry) since all methods overwrite
124
 */
125
static apr_byte_t oidc_proto_token_endpoint_request_auth(request_rec *r, oidc_cfg_t *cfg,
126
               const oidc_provider_t *provider, apr_table_t *params,
127
258
               char **basic_auth, char **bearer_auth) {
128
129
258
  return oidc_proto_token_endpoint_auth(
130
258
      r, cfg, oidc_cfg_provider_token_endpoint_auth_get(provider),
131
258
      oidc_cfg_provider_token_endpoint_auth_alg_get(provider), oidc_cfg_provider_client_id_get(provider),
132
258
      oidc_cfg_provider_client_secret_get(provider), oidc_cfg_provider_client_keys_get(provider),
133
258
      oidc_proto_profile_token_endpoint_auth_aud(provider), params, NULL, basic_auth, bearer_auth);
134
258
}
135
136
/*
137
 * retry the token endpoint call with a new DPoP header that carries the server-provided nonce;
138
 * on success, replaces *j_result with the freshly decoded response
139
 */
140
static apr_byte_t oidc_proto_token_endpoint_dpop_retry(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
141
                   apr_table_t *params, char **basic_auth, char **bearer_auth,
142
                   apr_hash_t *response_hdrs, char **response,
143
0
                   oidc_json_t **j_result) {
144
145
0
  char *dpop = NULL;
146
147
  /* without response headers there is no server-provided DPoP nonce to pick up (DPoP is disabled) */
148
0
  if (response_hdrs == NULL)
149
0
    return FALSE;
150
151
0
  if (oidc_proto_dpop_use_nonce(r, cfg, *j_result, response_hdrs,
152
0
              oidc_cfg_provider_token_endpoint_url_get(provider), "POST", NULL, &dpop) == FALSE)
153
0
    return FALSE;
154
155
  /* refresh the client authentication: a client assertion carries a one-time jti and a short-lived
156
   * exp/iat, so replaying the assertion of the failed call risks a rejection by the OP */
157
0
  if (oidc_proto_token_endpoint_request_auth(r, cfg, provider, params, basic_auth, bearer_auth) == FALSE)
158
0
    return FALSE;
159
160
0
  if (oidc_proto_token_endpoint_call(r, cfg, provider, params, *basic_auth, *bearer_auth, dpop, response,
161
0
             response_hdrs) == FALSE)
162
0
    return FALSE;
163
164
0
  oidc_json_decref(*j_result);
165
0
  *j_result = NULL;
166
167
0
  return oidc_json_decode_and_check_error(r, *response, j_result);
168
0
}
169
170
/*
171
 * parse a successful token endpoint response and validate the returned token type against the DPoP mode
172
 */
173
static apr_byte_t oidc_proto_token_endpoint_response_parse(request_rec *r, const oidc_provider_t *provider,
174
                 const oidc_json_t *j_result, char **id_token,
175
                 char **access_token, char **token_type, int *expires_in,
176
0
                 char **refresh_token, char **scope) {
177
178
0
  const oidc_json_t *j_expires_in = NULL;
179
180
0
  oidc_json_object_get_string(r->pool, j_result, OIDC_PROTO_ID_TOKEN, id_token, NULL);
181
0
  oidc_json_object_get_string(r->pool, j_result, OIDC_PROTO_ACCESS_TOKEN, access_token, NULL);
182
0
  oidc_json_object_get_string(r->pool, j_result, OIDC_PROTO_TOKEN_TYPE, token_type, NULL);
183
184
  /* check if DPoP is required */
185
0
  if ((oidc_proto_profile_dpop_mode_get(provider) == OIDC_DPOP_MODE_REQUIRED) &&
186
0
      ((*token_type == NULL) || (_oidc_strnatcasecmp(*token_type, OIDC_PROTO_DPOP) != 0))) {
187
0
    oidc_error(r, "access token type is \"%s\" but \"%s\" is required",
188
0
         *token_type ? *token_type : "(null)", OIDC_PROTO_DPOP);
189
0
    return FALSE;
190
0
  }
191
192
  /* check the new token type */
193
0
  if ((*token_type != NULL) && (oidc_proto_validate_token_type(r, provider, *token_type) == FALSE)) {
194
0
    oidc_warn(r, "access token type \"%s\" did not validate, dropping it", *token_type);
195
0
    *access_token = NULL;
196
0
    *token_type = NULL;
197
0
  }
198
199
  /* get the access token expires_in value; cater for string values (old Microsoft Entra ID / Azure AD) */
200
0
  *expires_in = -1;
201
0
  j_expires_in = oidc_json_object_get(j_result, OIDC_PROTO_EXPIRES_IN);
202
0
  if (oidc_json_is_string(j_expires_in)) {
203
0
    *expires_in = _oidc_str_to_int(oidc_json_string_value(j_expires_in), -1);
204
0
  } else if (oidc_json_is_integer(j_expires_in)) {
205
    /* clamp into int range so a maliciously huge OP value can't silently truncate to a small/negative TTL
206
     */
207
0
    oidc_json_int_t v = oidc_json_integer_value(j_expires_in);
208
0
    if (v > INT_MAX)
209
0
      *expires_in = INT_MAX;
210
0
    else if (v < INT_MIN)
211
0
      *expires_in = INT_MIN;
212
0
    else
213
0
      *expires_in = (int)v;
214
0
  }
215
216
0
  oidc_json_object_get_string(r->pool, j_result, OIDC_PROTO_REFRESH_TOKEN, refresh_token, NULL);
217
0
  oidc_json_object_get_string(r->pool, j_result, OIDC_PROTO_SCOPE, scope, NULL);
218
219
0
  return TRUE;
220
0
}
221
222
/*
223
 * send a code/refresh request to the token endpoint and return the parsed contents
224
 */
225
apr_byte_t oidc_proto_token_endpoint_request(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
226
               apr_table_t *params, char **id_token, char **access_token,
227
258
               char **token_type, int *expires_in, char **refresh_token, char **scope) {
228
229
258
  apr_byte_t rv = FALSE;
230
258
  char *basic_auth = NULL;
231
258
  char *bearer_auth = NULL;
232
258
  char *response = NULL;
233
258
  char *dpop = NULL;
234
258
  apr_hash_t *response_hdrs = NULL;
235
258
  oidc_json_t *j_result = NULL;
236
237
  /* add the token endpoint authentication credentials */
238
258
  if (oidc_proto_token_endpoint_request_auth(r, cfg, provider, params, &basic_auth, &bearer_auth) == FALSE)
239
0
    goto end;
240
241
  /* add any configured extra static parameters to the token endpoint */
242
258
  oidc_util_table_add_query_encoded_params(r->pool, params,
243
258
             oidc_cfg_provider_token_endpoint_params_get(provider));
244
245
  /* set up the DPoP header for the initial request if DPoP is enabled */
246
258
  if (oidc_proto_token_endpoint_dpop_prepare(r, cfg, provider, &response_hdrs, &dpop) == FALSE)
247
0
    goto end;
248
249
  /* send the request to the token endpoint */
250
258
  if (oidc_proto_token_endpoint_call(r, cfg, provider, params, basic_auth, bearer_auth, dpop, &response,
251
258
             response_hdrs) == FALSE)
252
258
    goto end;
253
254
  /* decode the response into a JSON object */
255
0
  if (oidc_json_decode_object_err(r, response, &j_result, TRUE) == FALSE)
256
0
    goto end;
257
258
  /* on a DPoP nonce error retry the call with a fresh nonce-bound DPoP header */
259
0
  if ((oidc_json_check_error(r, j_result) == TRUE) &&
260
0
      (oidc_proto_token_endpoint_dpop_retry(r, cfg, provider, params, &basic_auth, &bearer_auth, response_hdrs,
261
0
              &response, &j_result) == FALSE))
262
0
    goto end;
263
264
0
  if (oidc_proto_token_endpoint_response_parse(r, provider, j_result, id_token, access_token, token_type,
265
0
                 expires_in, refresh_token, scope) == FALSE)
266
0
    goto end;
267
268
0
  rv = TRUE;
269
270
258
end:
271
272
258
  if (j_result)
273
0
    oidc_json_decref(j_result);
274
275
258
  return rv;
276
0
}
277
278
/*
279
 * refreshes the access_token/id_token /refresh_token received from the OP using the refresh_token
280
 */
281
apr_byte_t oidc_proto_token_refresh_request(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
282
              const char *rtoken, char **id_token, char **access_token, char **token_type,
283
1
              int *expires_in, char **refresh_token, char **scope) {
284
285
1
  oidc_debug(r, "enter");
286
287
  /* assemble the parameters for a call to the token endpoint */
288
1
  apr_table_t *params = apr_table_make(r->pool, 5);
289
1
  apr_table_setn(params, OIDC_PROTO_GRANT_TYPE, OIDC_PROTO_GRANT_TYPE_REFRESH_TOKEN);
290
1
  apr_table_setn(params, OIDC_PROTO_REFRESH_TOKEN, rtoken);
291
1
  apr_table_setn(params, OIDC_PROTO_SCOPE, oidc_cfg_provider_scope_get(provider));
292
293
1
  return oidc_proto_token_endpoint_request(r, cfg, provider, params, id_token, access_token, token_type,
294
1
             expires_in, refresh_token, scope);
295
1
}