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/logout.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 "metrics.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
37
#define OIDC_DONT_REVOKE_TOKENS_BEFORE_LOGOUT_ENVVAR "OIDC_DONT_REVOKE_TOKENS_BEFORE_LOGOUT"
52
53
0
#define OIDC_TOKEN_REVOCATION_AUD_ENV_VAR "OIDC_TOKEN_REVOCATION_AUD"
54
55
/*
56
 * revoke a single token of the indicated type at the RFC 7009 revocation endpoint,
57
 * doing nothing when the session does not hold such a token
58
 */
59
static void oidc_logout_revoke_one_token(request_rec *r, oidc_cfg_t *c, const oidc_provider_t *provider,
60
           apr_table_t *params, const char *basic_auth, const char *bearer_auth,
61
0
           const char *token_type_hint, const char *token) {
62
0
  char *response = NULL;
63
64
0
  if (token == NULL)
65
0
    return;
66
67
0
  apr_table_setn(params, OIDC_PROTO_TOKEN_TYPE_HINT, token_type_hint);
68
0
  apr_table_setn(params, OIDC_PROTO_TOKEN, token);
69
70
0
  if (oidc_http_post_form(r, oidc_cfg_provider_revocation_endpoint_url_get(provider), params, basic_auth,
71
0
        bearer_auth, NULL, oidc_cfg_provider_ssl_validate_server_get(provider), &response, NULL,
72
0
        NULL, oidc_cfg_http_timeout_long_get(c), oidc_cfg_outgoing_proxy_get(c),
73
0
        oidc_cfg_dir_pass_cookies_get(r),
74
0
        oidc_cfg_provider_token_endpoint_tls_client_cert_get(provider),
75
0
        oidc_cfg_provider_token_endpoint_tls_client_key_get(provider),
76
0
        oidc_cfg_provider_token_endpoint_tls_client_key_pwd_get(provider)) == FALSE) {
77
0
    OIDC_METRICS_COUNTER_INC(r, c, OM_PROVIDER_REVOCATION_ERROR);
78
0
    oidc_warn(r, "revoking %s failed", token_type_hint);
79
0
  }
80
81
0
  apr_table_unset(params, OIDC_PROTO_TOKEN_TYPE_HINT);
82
0
  apr_table_unset(params, OIDC_PROTO_TOKEN);
83
0
}
84
85
/*
86
 * revoke refresh token and access token stored in the session if the
87
 * OP has an RFC 7009 compliant token revocation endpoint
88
 */
89
37
static void oidc_logout_revoke_tokens(request_rec *r, oidc_cfg_t *c, const oidc_session_t *session) {
90
91
37
  char *basic_auth = NULL;
92
37
  char *bearer_auth = NULL;
93
37
  apr_table_t *params = NULL;
94
37
  oidc_provider_t *provider = NULL;
95
96
37
  oidc_debug(r, "enter");
97
98
37
  if (oidc_get_provider_from_session(r, c, session, &provider) == FALSE)
99
0
    goto out;
100
101
37
  if (apr_table_get(r->subprocess_env, OIDC_DONT_REVOKE_TOKENS_BEFORE_LOGOUT_ENVVAR) != NULL)
102
0
    goto out;
103
104
37
  oidc_debug(r, "revocation_endpoint=%s",
105
37
       oidc_cfg_provider_revocation_endpoint_url_get(provider)
106
37
           ? oidc_cfg_provider_revocation_endpoint_url_get(provider)
107
37
           : "(null)");
108
109
37
  if ((oidc_cfg_provider_revocation_endpoint_url_get(provider) == NULL) ||
110
0
      (_oidc_strcmp(oidc_cfg_provider_revocation_endpoint_url_get(provider), "") == 0))
111
37
    goto out;
112
113
0
  params = apr_table_make(r->pool, 4);
114
115
  // add the token endpoint authentication credentials to the revocation endpoint call...
116
0
  if (oidc_proto_token_endpoint_auth(
117
0
    r, c, oidc_cfg_provider_token_endpoint_auth_get(provider),
118
0
    oidc_cfg_provider_token_endpoint_auth_alg_get(provider), oidc_cfg_provider_client_id_get(provider),
119
0
    oidc_cfg_provider_client_secret_get(provider), oidc_cfg_provider_client_keys_get(provider),
120
0
    oidc_proto_profile_revocation_endpoint_auth_aud(
121
0
        provider, apr_table_get(r->subprocess_env, OIDC_TOKEN_REVOCATION_AUD_ENV_VAR)),
122
0
    params, NULL, &basic_auth, &bearer_auth) == FALSE)
123
0
    goto out;
124
125
0
  oidc_logout_revoke_one_token(r, c, provider, params, basic_auth, bearer_auth, OIDC_PROTO_REFRESH_TOKEN,
126
0
             oidc_session_get_refresh_token(r, session));
127
0
  oidc_logout_revoke_one_token(r, c, provider, params, basic_auth, bearer_auth, OIDC_PROTO_ACCESS_TOKEN,
128
0
             oidc_session_get_access_token(r, session));
129
130
37
out:
131
132
37
  oidc_debug(r, "leave");
133
37
}
134
135
static apr_byte_t oidc_logout_cleanup_by_sid(request_rec *r, char *sid, oidc_cfg_t *cfg,
136
178
               const oidc_provider_t *provider, apr_byte_t revoke_tokens) {
137
138
178
  char *uuid = NULL;
139
178
  oidc_session_t session;
140
178
  apr_byte_t loaded = FALSE;
141
142
  /* zeroed because the cache load below reports a missing session entry as success without
143
   * touching the struct: a sid/sub index entry can outlive the session it points to (superseded
144
   * login, eviction) and extract must then see a NULL state, not indeterminate stack memory */
145
178
  _oidc_memset(&session, 0, sizeof(oidc_session_t));
146
147
178
  oidc_debug(r, "enter (sid=%s,iss=%s)", sid, oidc_cfg_provider_issuer_get(provider));
148
149
  /* A sub-based logout may span hosts that share both the cache and OIDCCryptoPassphrase. */
150
151
178
  sid = oidc_response_make_sid_iss_unique(r, sid, oidc_cfg_provider_issuer_get(provider));
152
178
  oidc_cache_get_sid(r, sid, &uuid);
153
178
  if (uuid == NULL) {
154
    // this may happen when we are the caller
155
178
    oidc_warn(
156
178
        r,
157
178
        "could not (or no longer) find a session based on sid/sub provided in logout token / parameter: %s",
158
178
        sid);
159
178
    r->user = "";
160
178
    return TRUE;
161
178
  }
162
163
  // load the session so we can (optionally) revoke its tokens and clean up all of its cache index entries
164
0
  if (oidc_cfg_session_type_get(cfg) != OIDC_SESSION_TYPE_CLIENT_COOKIE)
165
0
    loaded = (oidc_session_load_cache_by_uuid(r, cfg, uuid, &session) != FALSE) &&
166
0
       (oidc_session_extract(r, &session) != FALSE);
167
168
  // revoke tokens if we can get a handle on those
169
0
  if ((loaded == TRUE) && (revoke_tokens == TRUE))
170
0
    oidc_logout_revoke_tokens(r, cfg, &session);
171
172
  // clear the session and both its sid/sub cache index entries (it may have been located via either)
173
0
  oidc_cache_set_sid(r, sid, NULL, 0);
174
0
  if (loaded == TRUE) {
175
0
    if (session.sid != NULL)
176
0
      oidc_cache_set_sid(r, session.sid, NULL, 0);
177
0
    if (session.sub != NULL)
178
0
      oidc_cache_set_sid(r, session.sub, NULL, 0);
179
0
    oidc_session_free(r, &session);
180
0
  }
181
0
  oidc_cache_set_session(r, uuid, NULL, 0);
182
183
0
  r->user = "";
184
0
  return FALSE;
185
178
}
186
187
static apr_uint32_t oidc_logout_transparent_pixel[17] = {
188
    0x474e5089, 0x0a1a0a0d, 0x0d000000, 0x52444849, 0x01000000, 0x01000000, 0x00000408, 0x0c1cb500, 0x00000002,
189
    0x4144490b, 0x639c7854, 0x0000cffa, 0x02010702, 0x71311c9a, 0x00000000, 0x444e4549, 0x826042ae};
190
191
10.0k
static apr_byte_t oidc_logout_is_front_channel(const char *logout_param_value) {
192
10.0k
  return ((logout_param_value != NULL) &&
193
10.0k
    ((_oidc_strcmp(logout_param_value, OIDC_GET_STYLE_LOGOUT_PARAM_VALUE) == 0) ||
194
10.0k
     (_oidc_strcmp(logout_param_value, OIDC_IMG_STYLE_LOGOUT_PARAM_VALUE) == 0)));
195
10.0k
}
196
197
9.88k
static apr_byte_t oidc_logout_is_back_channel(const char *logout_param_value) {
198
9.88k
  return ((logout_param_value != NULL) &&
199
9.88k
    (_oidc_strcmp(logout_param_value, OIDC_BACKCHANNEL_STYLE_LOGOUT_PARAM_VALUE) == 0));
200
9.88k
}
201
202
/* Resolve the front-channel provider from sid/iss; preserve sid even when resolution fails. */
203
18
static oidc_provider_t *oidc_logout_request_front_channel_provider(request_rec *r, oidc_cfg_t *c, char **sid) {
204
205
18
  char *iss = NULL;
206
18
  oidc_provider_t *provider = NULL;
207
208
18
  if (oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_SID, sid) == FALSE)
209
5
    return NULL;
210
211
13
  if (oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_ISS, &iss) != FALSE)
212
3
    return oidc_get_provider_for_issuer(r, c, iss, FALSE);
213
214
  /*
215
   * Microsoft Entra ID / Azure AD seems to be such a non spec compliant provider.
216
   * In this case try our luck with the static config if possible.
217
   */
218
10
  oidc_debug(r, "OP did not provide an iss as parameter");
219
10
  if (oidc_provider_static_config(r, c, &provider) == FALSE)
220
0
    return NULL;
221
10
  return provider;
222
10
}
223
224
/*
225
 * Clear local state for front-channel logout, but never revoke tokens: the unsigned request and
226
 * third-party iframe provide no proof that the caller owns the named session.
227
 */
228
static int oidc_logout_request_front_channel(request_rec *r, oidc_cfg_t *c, const char *url,
229
19
               apr_byte_t no_session_provided) {
230
231
19
  if (no_session_provided) {
232
18
    char *sid = NULL;
233
18
    const oidc_provider_t *provider = oidc_logout_request_front_channel_provider(r, c, &sid);
234
18
    if (provider != NULL)
235
13
      oidc_logout_cleanup_by_sid(r, sid, c, provider, FALSE);
236
5
    else if (sid != NULL)
237
0
      oidc_info(r, "No provider for front channel logout found");
238
18
  }
239
240
  /* set recommended cache control headers */
241
19
  oidc_http_set_no_cache_headers(r);
242
19
  oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_P3P, "CAO PSA OUR");
243
19
  oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_EXPIRES, "0");
244
19
  oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_X_FRAME_OPTIONS, oidc_cfg_logout_x_frame_options_get(c));
245
246
  /* see if this is PF-PA style logout in which case we return a transparent pixel */
247
19
  const char *accept = oidc_http_hdr_in_accept_get(r);
248
19
  if ((_oidc_strcmp(url, OIDC_IMG_STYLE_LOGOUT_PARAM_VALUE) == 0) ||
249
11
      (accept && _oidc_strstr(accept, OIDC_HTTP_CONTENT_TYPE_IMAGE_PNG))) {
250
8
    return oidc_util_http_content_prep(r, (const char *)&oidc_logout_transparent_pixel,
251
8
               sizeof(oidc_logout_transparent_pixel),
252
8
               OIDC_HTTP_CONTENT_TYPE_IMAGE_PNG);
253
8
  }
254
255
  /* standard HTTP based logout: should be called in an iframe from the OP */
256
11
  return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Logged Out", NULL, NULL,
257
11
             "<p>Logged Out</p>");
258
19
}
259
260
/*
261
 * handle a local logout
262
 */
263
int oidc_logout_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session, const char *url,
264
149
      apr_byte_t revoke_tokens) {
265
266
149
  apr_byte_t no_session_provided = TRUE;
267
268
149
  oidc_debug(r, "enter (url=%s)", url);
269
270
  /*
271
   * if there's no remote_user then there's no (stored) session to kill; note that revoking the
272
   * tokens is tied to having one, i.e. to the caller having proven that the session is theirs
273
   */
274
149
  if (session->remote_user != NULL) {
275
37
    no_session_provided = FALSE;
276
37
    if (revoke_tokens)
277
37
      oidc_logout_revoke_tokens(r, c, session);
278
37
  }
279
280
  /*
281
   * remove session state (cq. cache entry and cookie)
282
   * always clear the session cookie because the cookie may be not sent (but still in the browser)
283
   * due to SameSite policies
284
   */
285
149
  oidc_session_kill(r, session);
286
287
  /* see if this is the OP calling us */
288
149
  if (oidc_logout_is_front_channel(url))
289
19
    return oidc_logout_request_front_channel(r, c, url, no_session_provided);
290
291
130
  oidc_http_set_no_cache_headers(r);
292
293
  /* see if we don't need to go somewhere special after killing the session locally */
294
130
  if (url == NULL)
295
18
    return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Logged Out", NULL, NULL,
296
18
               "<p>Logged Out</p>");
297
298
  /* send the user to the specified where-to-go-after-logout URL */
299
112
  oidc_http_hdr_out_location_set(r, url);
300
301
112
  return HTTP_MOVED_TEMPORARILY;
302
130
}
303
304
/*
305
 * handle a backchannel logout
306
 */
307
230
#define OIDC_EVENTS_BLOGOUT_KEY "http://schemas.openid.net/event/backchannel-logout"
308
309
/* a back-channel logout request must not repeat the logout token */
310
static const char *const OIDC_LOGOUT_NO_REPEAT[] = {OIDC_PROTO_LOGOUT_TOKEN, NULL};
311
312
9.25k
static int oidc_logout_backchannel_read_token(request_rec *r, const char **logout_token) {
313
9.25k
  apr_table_t *params = apr_table_make(r->pool, 8);
314
9.25k
  if (oidc_util_read_post_params_reject_dup(r, params, FALSE, NULL, OIDC_LOGOUT_NO_REPEAT) == FALSE) {
315
13
    oidc_error(r, "could not read POST-ed parameters to the logout endpoint");
316
13
    return HTTP_BAD_REQUEST;
317
13
  }
318
9.24k
  *logout_token = apr_table_get(params, OIDC_PROTO_LOGOUT_TOKEN);
319
9.24k
  if (*logout_token == NULL) {
320
2.04k
    oidc_error(r, "backchannel lggout endpoint was called but could not find a parameter named \"%s\"",
321
2.04k
         OIDC_PROTO_LOGOUT_TOKEN);
322
2.04k
    return HTTP_BAD_REQUEST;
323
2.04k
  }
324
7.19k
  return OK;
325
9.24k
}
326
327
static int oidc_logout_backchannel_parse_jwt(request_rec *r, oidc_cfg_t *cfg, const char *logout_token,
328
7.19k
               oidc_jwt_t **jwt) {
329
7.19k
  oidc_jose_error_t err;
330
7.19k
  oidc_jwk_t *jwk = NULL;
331
7.19k
  char *alg = NULL;
332
333
  /*
334
   * The issuer is unknown before decryption. Size the client-secret-derived key from the JOSE
335
   * header and try it alongside the RP private keys.
336
   */
337
7.19k
  oidc_proto_jwt_header_peek(r, logout_token, &alg, NULL, NULL);
338
7.19k
  oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(oidc_cfg_provider_get(cfg)),
339
7.19k
               oidc_alg2keysize(alg), OIDC_JOSE_ALG_SHA256, TRUE, &jwk);
340
341
7.19k
  if (oidc_jwt_parse(r->pool, logout_token, jwt,
342
7.19k
         oidc_util_key_symmetric_merge(r->pool, oidc_cfg_private_keys_get(cfg), jwk), FALSE,
343
7.19k
         &err) == FALSE) {
344
2.80k
    oidc_error(r, "oidc_jwt_parse failed: %s", oidc_jose_e2s(r->pool, err));
345
2.80k
    oidc_jwk_destroy(jwk);
346
2.80k
    return HTTP_BAD_REQUEST;
347
2.80k
  }
348
349
4.38k
  oidc_jwk_destroy(jwk);
350
351
4.38k
  if (((*jwt)->header.alg == NULL) || (_oidc_strcmp((*jwt)->header.alg, "none") == 0)) {
352
1
    oidc_error(r, "logout token is not signed");
353
1
    return HTTP_BAD_REQUEST;
354
1
  }
355
4.38k
  return OK;
356
4.38k
}
357
358
static int oidc_logout_backchannel_get_provider(request_rec *r, oidc_cfg_t *cfg, oidc_jwt_t *jwt,
359
4.38k
            oidc_provider_t **provider) {
360
4.38k
  *provider = oidc_get_provider_for_issuer(r, cfg, jwt->payload.iss, FALSE);
361
4.38k
  if (*provider == NULL) {
362
0
    oidc_error(r, "no provider found for issuer: %s", jwt->payload.iss);
363
0
    return HTTP_BAD_REQUEST;
364
0
  }
365
4.38k
  if ((oidc_cfg_provider_id_token_signed_response_alg_get(*provider) != NULL) &&
366
0
      (_oidc_strcmp(oidc_cfg_provider_id_token_signed_response_alg_get(*provider), jwt->header.alg) != 0)) {
367
0
    oidc_error(r, "logout token is signed using wrong algorithm: %s != %s", jwt->header.alg,
368
0
         oidc_cfg_provider_id_token_signed_response_alg_get(*provider));
369
0
    return HTTP_BAD_REQUEST;
370
0
  }
371
4.38k
  return OK;
372
4.38k
}
373
374
static int oidc_logout_backchannel_verify_jwt(request_rec *r, oidc_cfg_t *cfg, oidc_jwt_t *jwt,
375
4.38k
                const oidc_provider_t *provider, const oidc_jwk_t *jwk) {
376
4.38k
  if (oidc_proto_jwt_verify(
377
4.38k
    r, cfg, jwt, oidc_cfg_provider_jwks_uri_get(provider),
378
4.38k
    oidc_cfg_provider_ssl_validate_server_get(provider),
379
4.38k
    oidc_util_key_symmetric_merge(r->pool, oidc_cfg_provider_verify_public_keys_get(provider), jwk),
380
4.38k
    oidc_cfg_provider_id_token_signed_response_alg_get(provider)) == FALSE) {
381
41
    oidc_error(r, "id_token signature could not be validated, aborting");
382
41
    return HTTP_BAD_REQUEST;
383
41
  }
384
  /* Back-Channel Logout 2.4 requires iat to bound replay; exp remains optional. */
385
4.34k
  if (oidc_proto_jwt_validate(
386
4.34k
    r, jwt, oidc_cfg_provider_validate_issuer_get(provider) ? oidc_cfg_provider_issuer_get(provider) : NULL,
387
4.34k
    FALSE, TRUE, oidc_cfg_provider_idtoken_iat_slack_get(provider)) == FALSE)
388
3.82k
    return HTTP_BAD_REQUEST;
389
  /* verify the "aud" and "azp" values */
390
524
  if (oidc_proto_idtoken_validate_aud_and_azp(r, cfg, provider, &jwt->payload) == FALSE)
391
290
    return HTTP_BAD_REQUEST;
392
234
  return OK;
393
524
}
394
395
static int oidc_logout_backchannel_check_jti_replay(request_rec *r, const oidc_provider_t *provider,
396
225
                const oidc_jwt_t *jwt) {
397
225
  char *jti = NULL;
398
225
  char *replay = NULL;
399
225
  apr_time_t jti_cache_duration;
400
401
225
  oidc_json_object_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_JTI, &jti, NULL);
402
403
  /* Back-Channel Logout 2.4 requires jti for replay detection. */
404
225
  if (jti == NULL) {
405
4
    oidc_error(r, "logout token does not contain the required \"%s\" claim", OIDC_CLAIM_JTI);
406
4
    return HTTP_BAD_REQUEST;
407
4
  }
408
409
221
  if (oidc_cache_get_jti(r, jti, &replay) == FALSE)
410
221
    oidc_warn(r,
411
221
        "cache lookup for logout-token \"%s\" replay detection failed; a backend error is "
412
221
        "treated as \"not seen\", so replay protection is not enforced for this request",
413
221
        OIDC_CLAIM_JTI);
414
221
  if (replay != NULL) {
415
6
    oidc_error(r,
416
6
         "the \"%s\" value (%s) passed in logout token was found in the cache already; "
417
6
         "possible replay attack!?",
418
6
         OIDC_CLAIM_JTI, jti);
419
6
    return HTTP_BAD_REQUEST;
420
6
  }
421
422
  /* jti cache duration is the configured replay prevention window for token issuance plus 10 seconds for safety
423
   */
424
215
  jti_cache_duration = apr_time_from_sec(oidc_cfg_provider_idtoken_iat_slack_get(provider) * 2 + 10);
425
  /* store it in the cache for the calculated duration */
426
215
  if (oidc_cache_set_jti(r, jti, jti, apr_time_now() + jti_cache_duration) == FALSE)
427
215
    oidc_warn(r,
428
215
        "failed to store logout-token \"%s\" in the cache; a subsequent replay of this logout "
429
215
        "token may go undetected",
430
215
        OIDC_CLAIM_JTI);
431
215
  return OK;
432
221
}
433
434
static int oidc_logout_backchannel_validate_claims(request_rec *r, const oidc_provider_t *provider, oidc_jwt_t *jwt,
435
234
               char **sid) {
436
234
  char *nonce = NULL;
437
234
  const oidc_json_t *events = NULL;
438
234
  const oidc_json_t *blogout = NULL;
439
234
  int rc = OK;
440
441
234
  events = oidc_json_object_get(jwt->payload.value.json, OIDC_CLAIM_EVENTS);
442
234
  if (events == NULL) {
443
4
    oidc_error(r, "\"%s\" claim could not be found in logout token", OIDC_CLAIM_EVENTS);
444
4
    return HTTP_BAD_REQUEST;
445
4
  }
446
230
  blogout = oidc_json_object_get(events, OIDC_EVENTS_BLOGOUT_KEY);
447
230
  if (!oidc_json_is_object(blogout)) {
448
4
    oidc_error(r, "\"%s\" object could not be found in \"%s\" claim", OIDC_EVENTS_BLOGOUT_KEY,
449
4
         OIDC_CLAIM_EVENTS);
450
4
    return HTTP_BAD_REQUEST;
451
4
  }
452
453
226
  oidc_json_object_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_NONCE, &nonce, NULL);
454
226
  if (nonce != NULL) {
455
1
    oidc_error(r, "rejecting logout request/token since it contains a \"%s\" claim", OIDC_CLAIM_NONCE);
456
1
    return HTTP_BAD_REQUEST;
457
1
  }
458
459
225
  rc = oidc_logout_backchannel_check_jti_replay(r, provider, jwt);
460
225
  if (rc != OK)
461
10
    return rc;
462
463
  /*
464
   * With back-channel logout enabled, a distinct sub gets an additional index; when sid equals
465
   * sub, the sid index already uses it. A sub-only token can then resolve the session, but not when
466
   * a distinct sid was stored without back-channel support.
467
   */
468
215
  oidc_json_object_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_SID, sid, NULL);
469
215
  if (*sid == NULL)
470
211
    *sid = jwt->payload.sub;
471
215
  if (*sid == NULL) {
472
50
    oidc_error(r, "no \"sub\" and no \"sid\" claim found in logout token");
473
50
    return HTTP_BAD_REQUEST;
474
50
  }
475
165
  return OK;
476
215
}
477
478
9.25k
static int oidc_logout_backchannel(request_rec *r, oidc_cfg_t *cfg) {
479
9.25k
  int rc = HTTP_BAD_REQUEST;
480
9.25k
  const char *logout_token = NULL;
481
9.25k
  oidc_jwt_t *jwt = NULL;
482
9.25k
  oidc_jwk_t *jwk = NULL;
483
9.25k
  oidc_provider_t *provider = NULL;
484
9.25k
  char *sid = NULL;
485
486
9.25k
  oidc_debug(r, "enter");
487
488
9.25k
  rc = oidc_logout_backchannel_read_token(r, &logout_token);
489
9.25k
  if (rc != OK)
490
2.05k
    goto out;
491
492
7.19k
  rc = oidc_logout_backchannel_parse_jwt(r, cfg, logout_token, &jwt);
493
7.19k
  if (rc != OK)
494
2.80k
    goto out;
495
496
4.38k
  rc = oidc_logout_backchannel_get_provider(r, cfg, jwt, &provider);
497
4.38k
  if (rc != OK)
498
0
    goto out;
499
500
4.38k
  if (oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(provider), 0, NULL, TRUE, &jwk) ==
501
4.38k
      FALSE) {
502
0
    rc = HTTP_BAD_REQUEST;
503
0
    goto out;
504
0
  }
505
506
4.38k
  rc = oidc_logout_backchannel_verify_jwt(r, cfg, jwt, provider, jwk);
507
4.38k
  if (rc != OK)
508
4.15k
    goto out;
509
510
234
  rc = oidc_logout_backchannel_validate_claims(r, provider, jwt, &sid);
511
234
  if (rc != OK)
512
69
    goto out;
513
514
  // a backchannel logout comes from the provider, so no need to revoke the tokens
515
165
  oidc_logout_cleanup_by_sid(r, sid, cfg, provider, FALSE);
516
517
165
  rc = OK;
518
519
9.25k
out:
520
521
9.25k
  if (rc == OK) {
522
165
    OIDC_METRICS_COUNTER_INC(r, cfg, OM_LOGOUT_BACKCHANNEL);
523
9.08k
  } else {
524
9.08k
    OIDC_METRICS_COUNTER_INC(r, cfg, OM_LOGOUT_BACKCHANNEL_ERROR);
525
9.08k
  }
526
527
9.25k
  if (jwk != NULL)
528
4.38k
    oidc_jwk_destroy(jwk);
529
9.25k
  if (jwt != NULL)
530
4.38k
    oidc_jwt_destroy(jwt);
531
532
9.25k
  oidc_http_set_no_cache_headers(r);
533
534
9.25k
  return rc;
535
165
}
536
537
0
#define OIDC_REFRESH_TOKENS_BEFORE_LOGOUT_ENVVAR "OIDC_REFRESH_TOKENS_BEFORE_LOGOUT"
538
539
/*
540
 * append a query string suffix to url, picking '?' or '&' based on whether
541
 * url already contains a query string
542
 */
543
0
static char *oidc_logout_url_append(request_rec *r, const char *url, const char *suffix) {
544
0
  const char *sep = (strchr(url, OIDC_CHAR_QUERY) != NULL) ? OIDC_STR_AMP : OIDC_STR_QUERY;
545
0
  return apr_pstrcat(r->pool, url, sep, suffix, NULL);
546
0
}
547
548
/*
549
 * apply the default and validate the post-logout redirect URL; on failure
550
 * sets *rv to the HTTP status the caller should return
551
 */
552
628
static apr_byte_t oidc_logout_validate_url(request_rec *r, const oidc_cfg_t *c, char **url, int *rv) {
553
554
628
  char *error_str = NULL;
555
628
  char *error_description = NULL;
556
557
628
  if ((*url == NULL) || (_oidc_strcmp(*url, "") == 0)) {
558
17
    *url = apr_pstrdup(r->pool, oidc_util_url_abs(r, c, oidc_cfg_default_slo_url_get(c)));
559
17
    return TRUE;
560
17
  }
561
562
  /* do input validation on the logout parameter value */
563
611
  if (oidc_validate_redirect_url(r, c, *url, OIDC_REDIRECT_URL_SAME_HOST, &error_str, &error_description) ==
564
611
      FALSE) {
565
499
    *rv = oidc_util_html_send_error(r, error_str, error_description, HTTP_BAD_REQUEST);
566
499
    return FALSE;
567
499
  }
568
112
  return TRUE;
569
611
}
570
571
/*
572
 * build the OP end_session_endpoint URL, populating id_token_hint
573
 * (optionally refreshed), post_logout_redirect_uri and any configured
574
 * extra logout request parameters
575
 */
576
static char *oidc_logout_build_op_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session,
577
0
            const oidc_provider_t *provider, const char *post_logout_url) {
578
579
0
  char *id_token_hint = NULL;
580
0
  char *s_logout_request = NULL;
581
582
0
  if (apr_table_get(r->subprocess_env, OIDC_REFRESH_TOKENS_BEFORE_LOGOUT_ENVVAR) != NULL) {
583
0
    if (oidc_refresh_token_grant(r, c, session, provider, NULL, NULL, &id_token_hint) == FALSE)
584
0
      oidc_warn(r, "id_token_hint could not be refreshed before logout");
585
0
  } else {
586
0
    id_token_hint = apr_pstrdup(r->pool, oidc_session_get_idtoken(r, session));
587
0
  }
588
589
0
  s_logout_request = apr_pstrdup(r->pool, oidc_cfg_provider_end_session_endpoint_get(provider));
590
0
  if (id_token_hint != NULL)
591
0
    s_logout_request = oidc_logout_url_append(
592
0
        r, s_logout_request,
593
0
        apr_psprintf(r->pool, OIDC_PROTO_ID_TOKEN_HINT "=%s", oidc_http_url_encode(r, id_token_hint)));
594
595
0
  if (post_logout_url != NULL)
596
0
    s_logout_request = oidc_logout_url_append(
597
0
        r, s_logout_request,
598
0
        apr_psprintf(r->pool, "post_logout_redirect_uri=%s", oidc_http_url_encode(r, post_logout_url)));
599
600
0
  if (oidc_cfg_provider_logout_request_params_get(provider) != NULL)
601
0
    s_logout_request =
602
0
        oidc_logout_url_append(r, s_logout_request, oidc_cfg_provider_logout_request_params_get(provider));
603
604
0
  return s_logout_request;
605
0
}
606
607
/*
608
 * perform (single) logout
609
 */
610
9.90k
int oidc_logout(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) {
611
612
  /* pickup the command or URL where the user wants to go after logout */
613
9.90k
  char *url = NULL;
614
9.90k
  oidc_provider_t *provider = NULL;
615
9.90k
  int rv = OK;
616
617
9.90k
  oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_LOGOUT, &url);
618
619
9.90k
  oidc_debug(r, "enter (url=%s)", url);
620
621
9.90k
  if (oidc_logout_is_front_channel(url))
622
19
    return oidc_logout_request(r, c, session, url, TRUE);
623
9.88k
  if (oidc_logout_is_back_channel(url))
624
9.25k
    return oidc_logout_backchannel(r, c);
625
626
628
  if (oidc_logout_validate_url(r, c, &url, &rv) == FALSE)
627
499
    return rv;
628
629
129
  if (oidc_get_provider_from_session(r, c, session, &provider) == FALSE)
630
129
    oidc_warn(r, "oidc_get_provider_from_session failed");
631
632
129
  if ((provider != NULL) && (oidc_cfg_provider_end_session_endpoint_get(provider) != NULL))
633
0
    url = oidc_logout_build_op_request(r, c, session, provider, url);
634
635
129
  return oidc_logout_request(r, c, session, url, TRUE);
636
628
}