/src/mod_auth_openidc/src/handle/response.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 "state.h" |
49 | | #include "util/util.h" |
50 | | #include "util/util_cfg.h" |
51 | | |
52 | | /* |
53 | | * redirect the browser to the session logout endpoint |
54 | | */ |
55 | 713 | static int oidc_response_redirect_parent_window_to_logout(request_rec *r, const oidc_cfg_t *c) { |
56 | | |
57 | 713 | oidc_debug(r, "enter"); |
58 | | |
59 | 713 | const char *java_script = |
60 | 713 | apr_psprintf(r->pool, |
61 | 713 | " <script type=\"text/javascript\">\n" |
62 | 713 | " window.top.location.href = '%s?session=logout';\n" |
63 | 713 | " </script>\n", |
64 | 713 | oidc_util_html_javascript_escape(r->pool, oidc_util_url_redirect_uri(r, c))); |
65 | | |
66 | 713 | return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Redirecting...", java_script, NULL, NULL); |
67 | 713 | } |
68 | | |
69 | | /* |
70 | | * handle an error returned by the OP |
71 | | */ |
72 | | static int oidc_response_authorization_error(request_rec *r, const oidc_cfg_t *c, const oidc_proto_state_t *proto_state, |
73 | 5.69k | const char *error, const char *error_description) { |
74 | 5.69k | const char *prompt = oidc_proto_state_get_prompt(proto_state); |
75 | 5.69k | if (prompt != NULL) |
76 | 713 | prompt = apr_pstrdup(r->pool, prompt); |
77 | 5.69k | if ((prompt != NULL) && (_oidc_strcmp(prompt, OIDC_PROTO_PROMPT_NONE) == 0)) { |
78 | 713 | return oidc_response_redirect_parent_window_to_logout(r, c); |
79 | 713 | } |
80 | 4.98k | return oidc_util_html_send_error(r, apr_psprintf(r->pool, "OpenID Connect Provider error: %s", error), |
81 | 4.98k | error_description, HTTP_BAD_REQUEST); |
82 | 5.69k | } |
83 | | |
84 | | /* handle the browser back on an authorization response */ |
85 | 7.48k | static apr_byte_t oidc_response_browser_back(request_rec *r, const char *r_state, const oidc_session_t *session) { |
86 | 7.48k | const char *s_state = NULL; |
87 | 7.48k | const char *o_url = NULL; |
88 | | |
89 | | /* see if we have an existing session and browser-back was used */ |
90 | 7.48k | if (session->remote_user == NULL) |
91 | | /* no session was established yet */ |
92 | 7.23k | return FALSE; |
93 | | |
94 | 245 | s_state = oidc_session_get_request_state(r, session); |
95 | 245 | if ((r_state == NULL) || (s_state == NULL) || (_oidc_strcmp(r_state, s_state) != 0)) |
96 | | /* state does not match with the state that was used to create the session earlier, no replay is going |
97 | | * on here */ |
98 | 245 | return FALSE; |
99 | | |
100 | | /* get the URL that was originally accessed by the user */ |
101 | 0 | o_url = oidc_session_get_original_url(r, session); |
102 | | /* log the browser back event detection */ |
103 | 0 | oidc_warn(r, "browser back detected, redirecting to original URL: %s", o_url); |
104 | | /* go back to the URL that he originally tried to access */ |
105 | 0 | oidc_http_hdr_out_location_set(r, o_url); |
106 | | |
107 | | /* signal that a browser back event was detected indeed and we handled this here */ |
108 | 0 | return TRUE; |
109 | 245 | } |
110 | | |
111 | | static char *_oidc_response_post_preserve_template_contents = NULL; |
112 | | |
113 | | /* |
114 | | * send an OpenID Connect authorization request to the specified provider preserving POST parameters using HTML5 storage |
115 | | */ |
116 | | apr_byte_t oidc_response_post_preserve_javascript(request_rec *r, const char *location, char **javascript, |
117 | 2.37k | char **javascript_method) { |
118 | | |
119 | 2.37k | if (oidc_cfg_dir_preserve_post_get(r) == 0) |
120 | 1.59k | return FALSE; |
121 | | |
122 | 780 | oidc_debug(r, "enter"); |
123 | | |
124 | 780 | oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); |
125 | | |
126 | 780 | const char *method = oidc_original_request_method(r, cfg, FALSE); |
127 | | |
128 | 780 | if (_oidc_strcmp(method, OIDC_METHOD_FORM_POST) != 0) |
129 | 0 | return FALSE; |
130 | | |
131 | | /* read the parameters that are POST-ed to us */ |
132 | 780 | apr_table_t *params = apr_table_make(r->pool, 8); |
133 | 780 | if (oidc_util_read_post_params(r, params, FALSE, NULL) == FALSE) { |
134 | 0 | oidc_error(r, "something went wrong when reading the POST parameters"); |
135 | 0 | return FALSE; |
136 | 0 | } |
137 | | |
138 | | /* collect the pairs and join them once: appending to the accumulated string per parameter would make |
139 | | * the pool memory used here quadratic in the number of (client-supplied) POST parameters */ |
140 | 780 | const apr_array_header_t *arr = apr_table_elts(params); |
141 | 780 | const apr_table_entry_t *elts = (const apr_table_entry_t *)arr->elts; |
142 | 780 | apr_array_header_t *pairs = apr_array_make(r->pool, arr->nelts, sizeof(const char *)); |
143 | 4.27k | for (int i = 0; i < arr->nelts; i++) { |
144 | 3.49k | APR_ARRAY_PUSH(pairs, const char *) = apr_psprintf( |
145 | 3.49k | r->pool, "'%s': '%s'", oidc_http_url_encode(r, elts[i].key), oidc_http_url_encode(r, elts[i].val)); |
146 | 3.49k | } |
147 | 780 | char *json = apr_psprintf(r->pool, "{ %s }", apr_array_pstrcat(r->pool, pairs, OIDC_CHAR_COMMA)); |
148 | | |
149 | 780 | if ((oidc_cfg_post_preserve_template_get(cfg) != NULL) && |
150 | 0 | (oidc_util_html_send_in_template( |
151 | 0 | r, oidc_cfg_post_preserve_template_get(cfg), &_oidc_response_post_preserve_template_contents, json, |
152 | 0 | OIDC_POST_PRESERVE_ESCAPE_NONE, location, OIDC_POST_PRESERVE_ESCAPE_JAVASCRIPT) == OK)) |
153 | 0 | return TRUE; |
154 | | |
155 | 780 | const char *jmethod = "preserveOnLoad()"; |
156 | 780 | const char *jscript = apr_psprintf( |
157 | 780 | r->pool, |
158 | 780 | " <script type=\"text/javascript\">\n" |
159 | 780 | " function %s {\n" |
160 | 780 | " sessionStorage.setItem('mod_auth_openidc_preserve_post_params', JSON.stringify(%s));\n" |
161 | 780 | " %s" |
162 | 780 | " }\n" |
163 | 780 | " </script>\n", |
164 | 780 | jmethod, json, |
165 | 780 | location |
166 | 780 | ? apr_psprintf(r->pool, "window.location='%s';\n", oidc_util_html_javascript_escape(r->pool, location)) |
167 | 780 | : ""); |
168 | | |
169 | 780 | if (javascript_method) |
170 | 780 | *javascript_method = apr_pstrdup(r->pool, jmethod); |
171 | 780 | if (javascript) |
172 | 780 | *javascript = apr_pstrdup(r->pool, jscript); |
173 | | |
174 | 780 | return TRUE; |
175 | 780 | } |
176 | | |
177 | | /* |
178 | | * restore POST parameters on original_url from HTML5 session storage |
179 | | */ |
180 | 0 | static int oidc_response_post_preserved_restore(request_rec *r, const char *original_url) { |
181 | |
|
182 | 0 | oidc_debug(r, "enter: original_url=%s", original_url); |
183 | |
|
184 | 0 | const char *method = "postOnLoad()"; |
185 | 0 | const char *script = |
186 | 0 | apr_psprintf(r->pool, |
187 | 0 | " <script type=\"text/javascript\">\n" |
188 | 0 | " function str_decode(string) {\n" |
189 | 0 | " try {\n" |
190 | 0 | " result = decodeURIComponent(string);\n" |
191 | 0 | " } catch (e) {\n" |
192 | 0 | " result = unescape(string);\n" |
193 | 0 | " }\n" |
194 | 0 | " return result;\n" |
195 | 0 | " }\n" |
196 | 0 | " function %s {\n" |
197 | 0 | " var mod_auth_openidc_preserve_post_params = " |
198 | 0 | "JSON.parse(sessionStorage.getItem('mod_auth_openidc_preserve_post_params'));\n" |
199 | 0 | "\t\t sessionStorage.removeItem('mod_auth_openidc_preserve_post_params');\n" |
200 | 0 | " for (var key in mod_auth_openidc_preserve_post_params) {\n" |
201 | 0 | " var input = document.createElement(\"input\");\n" |
202 | 0 | " input.type = \"hidden\";\n" |
203 | 0 | " input.name = str_decode(key);\n" |
204 | 0 | " input.value = str_decode(mod_auth_openidc_preserve_post_params[key]);\n" |
205 | 0 | " document.forms[0].appendChild(input);\n" |
206 | 0 | " }\n" |
207 | 0 | " document.forms[0].action = \"%s\";\n" |
208 | 0 | " HTMLFormElement.prototype.submit.call(document.forms[0]);\n" |
209 | 0 | " }\n" |
210 | 0 | " </script>\n", |
211 | 0 | method, oidc_util_html_javascript_escape(r->pool, original_url)); |
212 | |
|
213 | 0 | const char *body = " <p>Restoring...</p>\n" |
214 | 0 | " <form method=\"post\"></form>\n"; |
215 | |
|
216 | 0 | return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Restoring...", script, method, body); |
217 | 0 | } |
218 | | |
219 | 1.19k | char *oidc_response_make_sid_iss_unique(request_rec *r, const char *sid, const char *issuer) { |
220 | 1.19k | return apr_psprintf(r->pool, "%s@%s", sid, issuer); |
221 | 1.19k | } |
222 | | |
223 | | /* |
224 | | * store resolved information in the session |
225 | | */ |
226 | | apr_byte_t oidc_response_save_in_session(request_rec *r, const oidc_cfg_t *c, oidc_session_t *session, |
227 | | const oidc_provider_t *provider, const char *remoteUser, const char *id_token, |
228 | | oidc_jwt_t *id_token_jwt, const char *s_userinfo_claims, |
229 | | oidc_json_t *userinfo_claims, const char *access_token, |
230 | | const char *access_token_type, const int expires_in, const char *refresh_token, |
231 | | const char *scope, const char *session_state, const char *state, |
232 | 1.01k | const char *original_url, const char *userinfo_jwt) { |
233 | | |
234 | | /* store the user in the session */ |
235 | 1.01k | session->remote_user = apr_pstrdup(r->pool, remoteUser); |
236 | | |
237 | | /* set the session expiry to the inactivity timeout */ |
238 | 1.01k | session->expiry = apr_time_now() + apr_time_from_sec(oidc_cfg_session_inactivity_timeout_get(c)); |
239 | | |
240 | | /* store the claims payload in the id_token for later reference */ |
241 | 1.01k | oidc_session_set_idtoken_claims(r, session, id_token_jwt->payload.value.json); |
242 | | |
243 | 1.01k | if (oidc_cfg_store_id_token_get(c)) { |
244 | | /* store the compact serialized representation of the id_token for later reference */ |
245 | 1.01k | oidc_session_set_idtoken(r, session, id_token); |
246 | 1.01k | } |
247 | | |
248 | | /* store the issuer in the session (at least needed for session mgmt and token refresh */ |
249 | 1.01k | oidc_session_set_issuer(r, session, oidc_cfg_provider_issuer_get(provider)); |
250 | | |
251 | | /* store the state and original URL in the session for handling browser-back more elegantly */ |
252 | 1.01k | oidc_session_set_request_state(r, session, state); |
253 | 1.01k | oidc_session_set_original_url(r, session, original_url); |
254 | | |
255 | 1.01k | if ((session_state != NULL) && (oidc_cfg_provider_check_session_iframe_get(provider) != NULL)) { |
256 | | /* store the session state and required parameters session management */ |
257 | 0 | oidc_session_set_session_state(r, session, session_state); |
258 | 0 | oidc_debug(r, |
259 | 0 | "session management enabled: stored session_state (%s), check_session_iframe (%s) and " |
260 | 0 | "client_id (%s) in the session", |
261 | 0 | session_state, oidc_cfg_provider_check_session_iframe_get(provider), |
262 | 0 | oidc_cfg_provider_client_id_get(provider)); |
263 | 1.01k | } else if (oidc_cfg_provider_check_session_iframe_get(provider) == NULL) { |
264 | 1.01k | oidc_debug( |
265 | 1.01k | r, "session management disabled: \"check_session_iframe\" is not set in provider configuration"); |
266 | 1.01k | } else { |
267 | 0 | oidc_debug(r, |
268 | 0 | "session management disabled: no \"session_state\" value is provided in the authentication " |
269 | 0 | "response even though \"check_session_iframe\" (%s) is set in the provider configuration", |
270 | 0 | oidc_cfg_provider_check_session_iframe_get(provider)); |
271 | 0 | } |
272 | | |
273 | | /* store the, possibly, provider specific userinfo_refresh_interval for performance reasons */ |
274 | 1.01k | oidc_session_set_userinfo_refresh_interval(r, session, |
275 | 1.01k | oidc_cfg_provider_userinfo_refresh_interval_get(provider)); |
276 | | |
277 | | /* store claims resolved from userinfo endpoint */ |
278 | 1.01k | oidc_userinfo_store_claims(r, c, session, provider, userinfo_claims, userinfo_jwt); |
279 | | |
280 | | /* see if we have an access_token */ |
281 | 1.01k | if (access_token != NULL) { |
282 | | /* store the access_token in the session context */ |
283 | 442 | oidc_session_set_access_token(r, session, access_token); |
284 | | /* store the access_token in the session context */ |
285 | 442 | oidc_session_set_access_token_type(r, session, access_token_type); |
286 | | /* store the associated expires_in value */ |
287 | 442 | oidc_session_set_access_token_expires(r, session, expires_in); |
288 | | /* reset the access token refresh timestamp */ |
289 | 442 | oidc_session_set_access_token_last_refresh(r, session, apr_time_now()); |
290 | 442 | } |
291 | | |
292 | | /* see if we have a refresh_token */ |
293 | 1.01k | if (refresh_token != NULL) { |
294 | | /* store the refresh_token in the session context */ |
295 | 0 | oidc_session_set_refresh_token(r, session, refresh_token); |
296 | 0 | } |
297 | | |
298 | | /* see if a scope was returned from the token endpoint */ |
299 | 1.01k | if (scope != NULL) { |
300 | | /* store the scope in the session context */ |
301 | 17 | oidc_session_set_scope(r, session, scope); |
302 | 17 | } |
303 | | |
304 | | /* store max session duration in the session as a hard cut-off expiry timestamp */ |
305 | 1.01k | apr_time_t session_expires = |
306 | 1.01k | (oidc_cfg_provider_session_max_duration_get(provider) == 0) |
307 | 1.01k | ? oidc_util_apr_time_from_sec(id_token_jwt->payload.exp) |
308 | 1.01k | : (apr_time_now() + apr_time_from_sec(oidc_cfg_provider_session_max_duration_get(provider))); |
309 | 1.01k | oidc_session_set_session_expires(r, session, session_expires); |
310 | | |
311 | 1.01k | oidc_debug(r, "oidc_provider_session_max_duration_get(provider) = %d, session_expires=%" APR_TIME_T_FMT, |
312 | 1.01k | oidc_cfg_provider_session_max_duration_get(provider), session_expires); |
313 | | |
314 | | /* log message about max session duration */ |
315 | 1.01k | oidc_log_session_expires(r, "session max lifetime", session_expires); |
316 | | |
317 | | /* store the domain for which this session is valid */ |
318 | 1.01k | oidc_session_set_cookie_domain(r, session, |
319 | 1.01k | oidc_cfg_cookie_domain_get(c) |
320 | 1.01k | ? oidc_cfg_cookie_domain_get(c) |
321 | 1.01k | : oidc_util_url_cur_host(r, oidc_cfg_x_forwarded_headers_get(c))); |
322 | | |
323 | 1.01k | char *sid = NULL; |
324 | 1.01k | char *sub = id_token_jwt->payload.sub; |
325 | 1.01k | oidc_debug(r, "provider->backchannel_logout_supported=%d", |
326 | 1.01k | oidc_cfg_provider_backchannel_logout_supported_get(provider)); |
327 | | /* Store sid even without back-channel support because front-channel logout may supply it. */ |
328 | 1.01k | oidc_jose_get_string(r->pool, id_token_jwt->payload.value.json, OIDC_CLAIM_SID, FALSE, &sid, NULL); |
329 | 1.01k | if (sid == NULL) |
330 | 1.01k | sid = sub; |
331 | 1.01k | session->sid = oidc_response_make_sid_iss_unique(r, sid, oidc_cfg_provider_issuer_get(provider)); |
332 | | |
333 | | /* Index by sub when back-channel logout may identify the session without sid. */ |
334 | 1.01k | if ((oidc_cfg_provider_backchannel_logout_supported_get(provider)) && (sub != NULL) && |
335 | 0 | (_oidc_strcmp(sid, sub) != 0)) |
336 | 0 | session->sub = oidc_response_make_sid_iss_unique(r, sub, oidc_cfg_provider_issuer_get(provider)); |
337 | | |
338 | | /* indicate that this is a newly created session */ |
339 | 1.01k | oidc_session_set_session_new(r, session, 1); |
340 | | |
341 | | /* store the session */ |
342 | 1.01k | return oidc_session_save(r, session, OIDC_SESSION_SAVE_NEW); |
343 | 1.01k | } |
344 | | |
345 | | /* |
346 | | * restore the state that was maintained between authorization request and response in an encrypted cookie |
347 | | */ |
348 | | static apr_byte_t oidc_response_proto_state_restore(request_rec *r, const oidc_cfg_t *c, const char *state, |
349 | 6.73k | oidc_proto_state_t **proto_state) { |
350 | | |
351 | 6.73k | oidc_debug(r, "enter"); |
352 | | |
353 | 6.73k | const char *cookieName = oidc_state_cookie_name(r, state); |
354 | | |
355 | | /* clean expired state cookies to avoid pollution */ |
356 | 6.73k | oidc_state_cookies_clean_expired(r, c, cookieName, FALSE); |
357 | | |
358 | | /* get the state cookie value first */ |
359 | 6.73k | const char *cookieValue = oidc_http_get_cookie(r, cookieName); |
360 | 6.73k | if (cookieValue == NULL) { |
361 | 24 | oidc_error(r, "no \"%s\" state cookie found: check domain and samesite cookie settings", cookieName); |
362 | 24 | return FALSE; |
363 | 24 | } |
364 | | |
365 | | /* clear state cookie because we don't need it anymore */ |
366 | 6.71k | oidc_http_set_cookie(r, cookieName, "", 0, OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r)); |
367 | | |
368 | 6.71k | *proto_state = oidc_proto_state_from_cookie(r, c, cookieValue); |
369 | 6.71k | if (*proto_state == NULL) |
370 | 0 | return FALSE; |
371 | | |
372 | 6.71k | const char *nonce = oidc_proto_state_get_nonce(*proto_state); |
373 | | |
374 | | /* calculate the hash of the browser fingerprint concatenated with the nonce */ |
375 | 6.71k | char *calc = oidc_state_browser_fingerprint(r, c, nonce); |
376 | | /* compare the calculated hash with the value provided in the authorization response */ |
377 | 6.71k | if (oidc_util_strcmp_const_time(calc, state) == FALSE) { |
378 | 0 | oidc_error( |
379 | 0 | r, |
380 | 0 | "calculated state from cookie does not match state parameter passed back in URL: \"%s\" != \"%s\"", |
381 | 0 | state, calc); |
382 | 0 | return FALSE; |
383 | 0 | } |
384 | | |
385 | 6.71k | apr_time_t ts = oidc_proto_state_get_timestamp(*proto_state); |
386 | | |
387 | | /* check that the timestamp is not beyond the valid interval */ |
388 | 6.71k | if (apr_time_now() > ts + apr_time_from_sec(oidc_cfg_state_timeout_get(c))) { |
389 | 0 | oidc_error(r, "state has expired"); |
390 | 0 | if ((oidc_cfg_default_sso_url_get(c) == NULL) || |
391 | 0 | (apr_table_get(r->subprocess_env, "OIDC_NO_DEFAULT_URL_ON_STATE_TIMEOUT") != NULL)) { |
392 | 0 | oidc_util_html_send_error( |
393 | 0 | r, "Invalid Authentication Response", |
394 | 0 | apr_psprintf(r->pool, |
395 | 0 | "This is due to a timeout; please restart your authentication session by " |
396 | 0 | "re-entering the URL/bookmark you originally wanted to access: %s", |
397 | 0 | oidc_proto_state_get_original_url(*proto_state)), |
398 | 0 | OK); |
399 | 0 | } |
400 | 0 | return FALSE; |
401 | 0 | } |
402 | | |
403 | | /* add the state */ |
404 | 6.71k | oidc_proto_state_set_state(*proto_state, state); |
405 | | |
406 | | /* log the restored state object */ |
407 | 6.71k | oidc_debug(r, "restored state: %s", oidc_proto_state_to_string(r, *proto_state)); |
408 | | |
409 | | /* we've made it */ |
410 | 6.71k | return TRUE; |
411 | 6.71k | } |
412 | | |
413 | | /* |
414 | | * helper function for basic/implicit client flows upon receiving an authorization response: |
415 | | * check that it matches the state stored in the browser and return the variables associated |
416 | | * with the state, such as original_url and OP oidc_provider_t pointer. |
417 | | */ |
418 | | static apr_byte_t oidc_response_match_state(request_rec *r, oidc_cfg_t *c, const char *state, |
419 | 7.48k | struct oidc_provider_t **provider, oidc_proto_state_t **proto_state) { |
420 | | |
421 | 7.48k | oidc_debug(r, "enter (state=%s)", state); |
422 | | |
423 | 7.48k | if ((state == NULL) || (_oidc_strcmp(state, "") == 0)) { |
424 | 746 | oidc_error(r, "state parameter is not set"); |
425 | 746 | return FALSE; |
426 | 746 | } |
427 | | |
428 | | /* check the state parameter against what we stored in a cookie */ |
429 | 6.73k | if (oidc_response_proto_state_restore(r, c, state, proto_state) == FALSE) { |
430 | 24 | oidc_error(r, "unable to restore state"); |
431 | 24 | return FALSE; |
432 | 24 | } |
433 | | |
434 | 6.71k | *provider = oidc_get_provider_for_issuer(r, c, oidc_proto_state_get_issuer(*proto_state), FALSE); |
435 | | |
436 | 6.71k | return (*provider != NULL); |
437 | 6.73k | } |
438 | | |
439 | | /* |
440 | | * handle the different flows (hybrid, implicit, Authorization Code) |
441 | | */ |
442 | | static apr_byte_t oidc_response_flows(request_rec *r, oidc_cfg_t *c, const oidc_proto_state_t *proto_state, |
443 | | oidc_provider_t *provider, apr_table_t *params, const char *response_mode, |
444 | 6.68k | oidc_jwt_t **jwt) { |
445 | | |
446 | 6.68k | apr_byte_t rc = FALSE; |
447 | | |
448 | 6.68k | const char *requested_response_type = oidc_proto_state_get_response_type(proto_state); |
449 | | |
450 | | /* handle the requested response type/mode */ |
451 | 6.68k | if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
452 | 6.68k | OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN_TOKEN)) { |
453 | 252 | rc = oidc_proto_response_code_idtoken_token(r, c, proto_state, provider, params, response_mode, jwt); |
454 | 6.43k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
455 | 6.43k | OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN)) { |
456 | 195 | rc = oidc_proto_response_code_idtoken(r, c, proto_state, provider, params, response_mode, jwt); |
457 | 6.23k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
458 | 6.23k | OIDC_PROTO_RESPONSE_TYPE_CODE_TOKEN)) { |
459 | 99 | rc = oidc_proto_response_code_token(r, c, proto_state, provider, params, response_mode, jwt); |
460 | 6.14k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, OIDC_PROTO_RESPONSE_TYPE_CODE)) { |
461 | 933 | rc = oidc_proto_response_code(r, c, proto_state, provider, params, response_mode, jwt); |
462 | 5.20k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
463 | 5.20k | OIDC_PROTO_RESPONSE_TYPE_IDTOKEN_TOKEN)) { |
464 | 915 | rc = oidc_proto_response_idtoken_token(r, c, proto_state, provider, params, response_mode, jwt); |
465 | 4.29k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, OIDC_PROTO_RESPONSE_TYPE_IDTOKEN)) { |
466 | 4.29k | rc = oidc_proto_response_idtoken(r, c, proto_state, provider, params, response_mode, jwt); |
467 | 4.29k | } else { |
468 | 0 | oidc_error(r, "unsupported response type: \"%s\"", requested_response_type); |
469 | 0 | } |
470 | | |
471 | 6.68k | if ((rc == FALSE) && (*jwt != NULL)) { |
472 | 73 | oidc_jwt_destroy(*jwt); |
473 | 73 | *jwt = NULL; |
474 | 73 | } |
475 | | |
476 | 6.68k | return rc; |
477 | 6.68k | } |
478 | | |
479 | | /* |
480 | | * set the unique user identifier that will be propagated in the Apache r->user and REMOTE_USER variables |
481 | | */ |
482 | | static apr_byte_t oidc_response_set_request_user(request_rec *r, const oidc_cfg_t *c, const oidc_provider_t *provider, |
483 | 1.01k | oidc_jwt_t *jwt, const oidc_json_t *userinfo_claims) { |
484 | | |
485 | 1.01k | const char *issuer = oidc_cfg_provider_issuer_get(provider); |
486 | 1.01k | char *claim_name = apr_pstrdup(r->pool, oidc_cfg_remote_user_claim_name_get(c)); |
487 | 1.01k | int n = (int)_oidc_strlen(claim_name); |
488 | 1.01k | apr_byte_t post_fix_with_issuer = (n > 0) && (claim_name[n - 1] == OIDC_CHAR_AT); |
489 | 1.01k | if (post_fix_with_issuer == TRUE) { |
490 | 1.01k | claim_name[n - 1] = '\0'; |
491 | 1.01k | issuer = (_oidc_strstr(issuer, "https://") == NULL) |
492 | 1.01k | ? apr_pstrdup(r->pool, issuer) |
493 | 1.01k | : apr_pstrdup(r->pool, issuer + _oidc_strlen("https://")); |
494 | 1.01k | } |
495 | | |
496 | | /* extract the username claim (default: "sub") from the id_token payload or user claims */ |
497 | 1.01k | apr_byte_t rc = FALSE; |
498 | 1.01k | char *remote_user = NULL; |
499 | 1.01k | if (userinfo_claims == NULL) { |
500 | 1.01k | rc = oidc_get_remote_user(r, claim_name, oidc_cfg_remote_user_claim_get(c)->reg_exp, |
501 | 1.01k | oidc_cfg_remote_user_claim_get(c)->replace, jwt->payload.value.json, |
502 | 1.01k | &remote_user); |
503 | 1.01k | } else { |
504 | 0 | oidc_json_t *claims = oidc_json_copy(userinfo_claims); |
505 | 0 | oidc_json_merge(r, jwt->payload.value.json, claims); |
506 | 0 | rc = oidc_get_remote_user(r, claim_name, oidc_cfg_remote_user_claim_get(c)->reg_exp, |
507 | 0 | oidc_cfg_remote_user_claim_get(c)->replace, claims, &remote_user); |
508 | 0 | oidc_json_decref(claims); |
509 | 0 | } |
510 | | |
511 | 1.01k | if ((rc == FALSE) || (remote_user == NULL)) { |
512 | 0 | oidc_error(r, |
513 | 0 | "" OIDCRemoteUserClaim " is set to \"%s\", but could not set the remote user based on the " |
514 | 0 | "requested claim \"%s\" and the available claims for the user", |
515 | 0 | oidc_cfg_remote_user_claim_name_get(c), claim_name); |
516 | 0 | return FALSE; |
517 | 0 | } |
518 | | |
519 | 1.01k | if (post_fix_with_issuer == TRUE) |
520 | 1.01k | remote_user = apr_psprintf(r->pool, "%s%s%s", remote_user, OIDC_STR_AT, issuer); |
521 | | |
522 | 1.01k | r->user = apr_pstrdup(r->pool, remote_user); |
523 | | |
524 | 1.01k | oidc_debug( |
525 | 1.01k | r, "set remote_user to \"%s\" based on claim: \"%s\"%s", r->user, oidc_cfg_remote_user_claim_name_get(c), |
526 | 1.01k | oidc_cfg_remote_user_claim_get(c)->reg_exp |
527 | 1.01k | ? apr_psprintf(r->pool, " and expression: \"%s\" and replace string: \"%s\"", |
528 | 1.01k | oidc_cfg_remote_user_claim_get(c)->reg_exp, oidc_cfg_remote_user_claim_get(c)->replace) |
529 | 1.01k | : ""); |
530 | | |
531 | 1.01k | return TRUE; |
532 | 1.01k | } |
533 | | |
534 | | static char *_oidc_response_post_restore_template_contents = NULL; |
535 | | |
536 | | /* |
537 | | * handle the case where the state parameter from the authorization response could not be matched |
538 | | */ |
539 | 770 | static int oidc_response_handle_state_mismatch(request_rec *r, const oidc_cfg_t *c) { |
540 | 770 | if (oidc_cfg_default_sso_url_get(c) != NULL) { |
541 | 0 | oidc_warn(r, |
542 | 0 | "invalid authorization response state; a default SSO URL is set, sending the user there: %s", |
543 | 0 | oidc_cfg_default_sso_url_get(c)); |
544 | 0 | oidc_http_hdr_out_location_set(r, oidc_util_url_abs(r, c, oidc_cfg_default_sso_url_get(c))); |
545 | 0 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_STATE_MISMATCH); |
546 | 0 | return HTTP_MOVED_TEMPORARILY; |
547 | 0 | } |
548 | | |
549 | 770 | oidc_error(r, "invalid authorization response state and no default SSO URL is set, sending an error..."); |
550 | | |
551 | | // if error text was already produced (e.g. state timeout) then just return with a 400 |
552 | 770 | if (apr_table_get(r->subprocess_env, OIDC_ERROR_ENVVAR) != NULL) { |
553 | 0 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_STATE_EXPIRED); |
554 | 0 | return HTTP_BAD_REQUEST; |
555 | 0 | } |
556 | | |
557 | 770 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_STATE_MISMATCH); |
558 | | |
559 | 770 | return oidc_util_html_send_error(r, "Invalid Authorization Response", |
560 | 770 | "Could not match the authorization response to an earlier request via " |
561 | 770 | "the state parameter and corresponding state cookie", |
562 | 770 | HTTP_BAD_REQUEST); |
563 | 770 | } |
564 | | |
565 | | /* |
566 | | * finalize a successful authorization response: restore preserved form-post data or redirect to the original URL |
567 | | */ |
568 | | static int oidc_response_finish_success(request_rec *r, const oidc_cfg_t *c, const char *original_url, |
569 | 942 | const char *original_method) { |
570 | | /* log the successful response */ |
571 | 942 | oidc_debug(r, "session created and stored, returning to original URL: %s, original method: %s", original_url, |
572 | 942 | original_method); |
573 | | |
574 | | /* check whether form post data was preserved; if so restore it */ |
575 | 942 | if (_oidc_strcmp(original_method, OIDC_METHOD_FORM_POST) == 0) { |
576 | 0 | if (oidc_cfg_post_restore_template_get(c) != NULL) |
577 | 0 | return oidc_util_html_send_in_template( |
578 | 0 | r, oidc_cfg_post_restore_template_get(c), &_oidc_response_post_restore_template_contents, |
579 | 0 | original_url, OIDC_POST_PRESERVE_ESCAPE_JAVASCRIPT, NULL, OIDC_POST_PRESERVE_ESCAPE_NONE); |
580 | 0 | return oidc_response_post_preserved_restore(r, original_url); |
581 | 0 | } |
582 | | |
583 | | /* now we've authenticated the user so go back to the URL that he originally tried to access */ |
584 | 942 | oidc_http_hdr_out_location_set(r, original_url); |
585 | | |
586 | | /* do the actual redirect to the original URL */ |
587 | 942 | return HTTP_MOVED_TEMPORARILY; |
588 | 942 | } |
589 | | |
590 | | /* |
591 | | * complete the handling of an authorization response by obtaining, parsing and verifying the |
592 | | * id_token and storing the authenticated user state in the session |
593 | | */ |
594 | | static int oidc_response_process(request_rec *r, oidc_cfg_t *c, oidc_session_t *session, apr_table_t *params, |
595 | 7.48k | const char *response_mode) { |
596 | 7.48k | int rc = -1; |
597 | 7.48k | oidc_provider_t *provider = NULL; |
598 | 7.48k | oidc_proto_state_t *proto_state = NULL; |
599 | 7.48k | oidc_jwt_t *id_token = NULL; |
600 | 7.48k | oidc_json_t *userinfo_claims = NULL; |
601 | 7.48k | int expires_in = 0; |
602 | 7.48k | char *userinfo_jwt = NULL; |
603 | 7.48k | const char *s_userinfo_claims = NULL; |
604 | 7.48k | const char *original_url = NULL; |
605 | 7.48k | const char *original_method = NULL; |
606 | 7.48k | const char *prompt = NULL; |
607 | | |
608 | 7.48k | oidc_debug(r, "enter, response_mode=%s", response_mode); |
609 | | |
610 | | /* see if this response came from a browser-back event */ |
611 | 7.48k | if (oidc_response_browser_back(r, apr_table_get(params, OIDC_PROTO_STATE), session) == TRUE) { |
612 | 0 | rc = HTTP_MOVED_TEMPORARILY; |
613 | 0 | goto end; |
614 | 0 | } |
615 | | |
616 | | /* match the returned state parameter against the state stored in the browser */ |
617 | 7.48k | if (oidc_response_match_state(r, c, apr_table_get(params, OIDC_PROTO_STATE), &provider, &proto_state) == |
618 | 7.48k | FALSE) { |
619 | 770 | rc = oidc_response_handle_state_mismatch(r, c); |
620 | 770 | goto end; |
621 | 770 | } |
622 | | |
623 | | /* see if the response is an error response */ |
624 | 6.71k | if (apr_table_get(params, OIDC_PROTO_ERROR) != NULL) { |
625 | 26 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_PROVIDER); |
626 | 26 | rc = oidc_response_authorization_error(r, c, proto_state, apr_table_get(params, OIDC_PROTO_ERROR), |
627 | 26 | apr_table_get(params, OIDC_PROTO_ERROR_DESCRIPTION)); |
628 | 26 | goto end; |
629 | 26 | } |
630 | | |
631 | | /* handle the code, implicit or hybrid flow */ |
632 | 6.68k | if (oidc_response_flows(r, c, proto_state, provider, params, response_mode, &id_token) == FALSE) { |
633 | 5.66k | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_PROTOCOL); |
634 | 5.66k | rc = oidc_response_authorization_error(r, c, proto_state, "Error in handling response type.", NULL); |
635 | 5.66k | goto end; |
636 | 5.66k | } |
637 | | |
638 | 1.01k | if (id_token == NULL) { |
639 | 0 | oidc_error(r, "no id_token was provided"); |
640 | 0 | rc = oidc_response_authorization_error(r, c, proto_state, "No id_token was provided.", NULL); |
641 | 0 | goto end; |
642 | 0 | } |
643 | | |
644 | 1.01k | expires_in = _oidc_str_to_int(apr_table_get(params, OIDC_PROTO_EXPIRES_IN), -1); |
645 | | |
646 | | /* |
647 | | * optionally resolve additional claims against the userinfo endpoint |
648 | | * parsed claims are not actually used here but need to be parsed anyway for error checking purposes |
649 | | */ |
650 | 1.01k | s_userinfo_claims = oidc_userinfo_retrieve_claims( |
651 | 1.01k | r, c, provider, apr_table_get(params, OIDC_PROTO_ACCESS_TOKEN), |
652 | 1.01k | apr_table_get(params, OIDC_PROTO_TOKEN_TYPE), NULL, id_token->payload.sub, &userinfo_claims, &userinfo_jwt); |
653 | | |
654 | | /* restore the original protected URL that the user was trying to access */ |
655 | 1.01k | original_url = oidc_proto_state_get_original_url(proto_state); |
656 | 1.01k | if (original_url != NULL) |
657 | 1.01k | original_url = apr_pstrdup(r->pool, original_url); |
658 | 1.01k | original_method = oidc_proto_state_get_original_method(proto_state); |
659 | 1.01k | if (original_method != NULL) |
660 | 1.01k | original_method = apr_pstrdup(r->pool, original_method); |
661 | 1.01k | prompt = oidc_proto_state_get_prompt(proto_state); |
662 | | |
663 | | /* set the user */ |
664 | 1.01k | if (oidc_response_set_request_user(r, c, provider, id_token, userinfo_claims) == FALSE) { |
665 | 0 | oidc_error(r, "remote user could not be set"); |
666 | 0 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_REMOTE_USER); |
667 | 0 | rc = oidc_response_authorization_error( |
668 | 0 | r, c, proto_state, "Remote user could not be set: contact the website administrator", NULL); |
669 | 0 | goto end; |
670 | 0 | } |
671 | | |
672 | 1.01k | oidc_debug(r, "set remote_user to \"%s\" in new session \"%s\"", r->user, session->uuid); |
673 | | |
674 | | /* For prompt=none, reject a different existing remote user. With no prior user, establish a new session. */ |
675 | 1.01k | if ((prompt != NULL) && (_oidc_strcmp(prompt, OIDC_PROTO_PROMPT_NONE) == 0) && (session->remote_user != NULL) && |
676 | 0 | (_oidc_strcmp(session->remote_user, r->user) != 0)) { |
677 | 0 | oidc_warn(r, "user set from new id_token is different from current one"); |
678 | 0 | rc = oidc_response_authorization_error(r, c, proto_state, "User changed!", NULL); |
679 | 0 | goto end; |
680 | 0 | } |
681 | | |
682 | | /* Reset after checks that need the old session and before storing the new authentication. */ |
683 | 1.01k | oidc_session_reset(r, c, session); |
684 | | |
685 | | /* Persist per-path settings only when session management may reuse them for silent reauthentication. */ |
686 | 1.01k | if (oidc_cfg_provider_check_session_iframe_get(provider) != NULL) { |
687 | 0 | oidc_session_set_path_auth_request_params(r, session, |
688 | 0 | oidc_proto_state_get_auth_request_params(proto_state)); |
689 | 0 | oidc_session_set_path_scope(r, session, oidc_proto_state_get_path_scope(proto_state)); |
690 | 0 | } |
691 | | |
692 | | /* store resolved information in the session */ |
693 | 1.01k | if (oidc_response_save_in_session( |
694 | 1.01k | r, c, session, provider, r->user, apr_table_get(params, OIDC_PROTO_ID_TOKEN), id_token, |
695 | 1.01k | s_userinfo_claims, userinfo_claims, apr_table_get(params, OIDC_PROTO_ACCESS_TOKEN), |
696 | 1.01k | apr_table_get(params, OIDC_PROTO_TOKEN_TYPE), expires_in, |
697 | 1.01k | apr_table_get(params, OIDC_PROTO_REFRESH_TOKEN), apr_table_get(params, OIDC_PROTO_SCOPE), |
698 | 1.01k | apr_table_get(params, OIDC_PROTO_SESSION_STATE), apr_table_get(params, OIDC_PROTO_STATE), original_url, |
699 | 1.01k | userinfo_jwt) == FALSE) { |
700 | 77 | rc = HTTP_INTERNAL_SERVER_ERROR; |
701 | 77 | goto end; |
702 | 77 | } |
703 | | |
704 | | /* check that we've actually authenticated a user; functions as error handling for oidc_get_remote_user */ |
705 | 942 | if (r->user == NULL) { |
706 | 0 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_REMOTE_USER); |
707 | 0 | rc = HTTP_UNAUTHORIZED; |
708 | 0 | goto end; |
709 | 0 | } |
710 | | |
711 | 942 | rc = oidc_response_finish_success(r, c, original_url, original_method); |
712 | | |
713 | 7.48k | end: |
714 | | |
715 | 7.48k | if (proto_state) |
716 | 6.71k | oidc_proto_state_destroy(proto_state); |
717 | 7.48k | if (id_token) |
718 | 1.01k | oidc_jwt_destroy(id_token); |
719 | 7.48k | if (userinfo_claims) |
720 | 0 | oidc_json_decref(userinfo_claims); |
721 | | |
722 | 7.48k | return rc; |
723 | 942 | } |
724 | | |
725 | | /* specification-defined parameters that must not be repeated in an authorization response */ |
726 | | static const char *const OIDC_RESPONSE_NO_REPEAT[] = {OIDC_PROTO_STATE, OIDC_PROTO_CODE, OIDC_PROTO_ACCESS_TOKEN, |
727 | | OIDC_PROTO_ID_TOKEN, NULL}; |
728 | | |
729 | | /* |
730 | | * handle an OpenID Connect Authorization Response using the POST (+fragment->POST) response_mode |
731 | | */ |
732 | 1.50k | int oidc_response_authorization_post(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) { |
733 | | |
734 | 1.50k | oidc_debug(r, "enter"); |
735 | | |
736 | | /* initialize local variables */ |
737 | 1.50k | const char *response_mode = NULL; |
738 | | |
739 | | /* read the parameters that are POST-ed to us */ |
740 | 1.50k | apr_table_t *params = apr_table_make(r->pool, 8); |
741 | 1.50k | if (oidc_util_read_post_params_reject_dup(r, params, FALSE, NULL, OIDC_RESPONSE_NO_REPEAT) == FALSE) { |
742 | 25 | oidc_error(r, "something went wrong when reading the POST parameters"); |
743 | 25 | return HTTP_BAD_REQUEST; |
744 | 25 | } |
745 | | |
746 | | /* see if we've got any POST-ed data at all */ |
747 | 1.47k | if ((apr_table_elts(params)->nelts < 1) || |
748 | 1.46k | ((apr_table_elts(params)->nelts == 1) && apr_table_get(params, OIDC_PROTO_RESPONSE_MODE) && |
749 | 75 | (_oidc_strcmp(apr_table_get(params, OIDC_PROTO_RESPONSE_MODE), OIDC_PROTO_RESPONSE_MODE_FRAGMENT) == 0))) { |
750 | 10 | return oidc_util_html_send_error( |
751 | 10 | r, "Invalid Request", |
752 | 10 | "You've hit an OpenID Connect Redirect URI with no parameters, this is an invalid request; you " |
753 | 10 | "should not open this URL in your browser directly, or have the server administrator use a " |
754 | 10 | "different " OIDCRedirectURI " setting.", |
755 | 10 | HTTP_INTERNAL_SERVER_ERROR); |
756 | 10 | } |
757 | | |
758 | | /* get the parameters */ |
759 | 1.46k | response_mode = apr_table_get(params, OIDC_PROTO_RESPONSE_MODE); |
760 | | |
761 | | /* do the actual implicit work */ |
762 | 1.46k | return oidc_response_process(r, c, session, params, |
763 | 1.46k | response_mode ? response_mode : OIDC_PROTO_RESPONSE_MODE_FORM_POST); |
764 | 1.47k | } |
765 | | |
766 | | /* |
767 | | * handle an OpenID Connect Authorization Response using the redirect response_mode |
768 | | */ |
769 | 6.08k | int oidc_response_authorization_redirect(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) { |
770 | | |
771 | 6.08k | oidc_debug(r, "enter"); |
772 | | |
773 | | /* read the parameters from the query string */ |
774 | 6.08k | apr_table_t *params = apr_table_make(r->pool, 8); |
775 | 6.08k | if (oidc_util_read_form_encoded_params_reject_dup(r, params, r->args, OIDC_RESPONSE_NO_REPEAT) == FALSE) |
776 | 73 | return HTTP_BAD_REQUEST; |
777 | | |
778 | | /* do the actual work */ |
779 | 6.01k | return oidc_response_process(r, c, session, params, OIDC_PROTO_RESPONSE_MODE_QUERY); |
780 | 6.08k | } |