/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 | 410 | static int oidc_response_redirect_parent_window_to_logout(request_rec *r, const oidc_cfg_t *c) { |
56 | | |
57 | 410 | oidc_debug(r, "enter"); |
58 | | |
59 | 410 | const char *java_script = |
60 | 410 | apr_psprintf(r->pool, |
61 | 410 | " <script type=\"text/javascript\">\n" |
62 | 410 | " window.top.location.href = '%s?session=logout';\n" |
63 | 410 | " </script>\n", |
64 | 410 | oidc_util_html_javascript_escape(r->pool, oidc_util_url_redirect_uri(r, c))); |
65 | | |
66 | 410 | return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Redirecting...", java_script, NULL, NULL); |
67 | 410 | } |
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 | 2.64k | const char *error, const char *error_description) { |
74 | 2.64k | const char *prompt = oidc_proto_state_get_prompt(proto_state); |
75 | 2.64k | if (prompt != NULL) |
76 | 410 | prompt = apr_pstrdup(r->pool, prompt); |
77 | 2.64k | if ((prompt != NULL) && (_oidc_strcmp(prompt, OIDC_PROTO_PROMPT_NONE) == 0)) { |
78 | 410 | return oidc_response_redirect_parent_window_to_logout(r, c); |
79 | 410 | } |
80 | 2.23k | return oidc_util_html_send_error(r, apr_psprintf(r->pool, "OpenID Connect Provider error: %s", error), |
81 | 2.23k | error_description, HTTP_BAD_REQUEST); |
82 | 2.64k | } |
83 | | |
84 | | /* handle the browser back on an authorization response */ |
85 | 3.84k | static apr_byte_t oidc_response_browser_back(request_rec *r, const char *r_state, const oidc_session_t *session) { |
86 | 3.84k | const char *s_state = NULL; |
87 | 3.84k | const char *o_url = NULL; |
88 | | |
89 | | /* see if we have an existing session and browser-back was used */ |
90 | 3.84k | if (session->remote_user == NULL) |
91 | | /* no session was established yet */ |
92 | 3.56k | return FALSE; |
93 | | |
94 | 286 | s_state = oidc_session_get_request_state(r, session); |
95 | 286 | 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 | 286 | 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 | 286 | } |
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 | 1.77k | char **javascript_method) { |
118 | | |
119 | 1.77k | if (oidc_cfg_dir_preserve_post_get(r) == 0) |
120 | 1.04k | return FALSE; |
121 | | |
122 | 738 | oidc_debug(r, "enter"); |
123 | | |
124 | 738 | oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); |
125 | | |
126 | 738 | const char *method = oidc_original_request_method(r, cfg, FALSE); |
127 | | |
128 | 738 | 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 | 738 | apr_table_t *params = apr_table_make(r->pool, 8); |
133 | 738 | 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 | 738 | const apr_array_header_t *arr = apr_table_elts(params); |
141 | 738 | const apr_table_entry_t *elts = (const apr_table_entry_t *)arr->elts; |
142 | 738 | apr_array_header_t *pairs = apr_array_make(r->pool, arr->nelts, sizeof(const char *)); |
143 | 3.28k | for (int i = 0; i < arr->nelts; i++) { |
144 | 2.54k | APR_ARRAY_PUSH(pairs, const char *) = apr_psprintf( |
145 | 2.54k | r->pool, "'%s': '%s'", oidc_http_url_encode(r, elts[i].key), oidc_http_url_encode(r, elts[i].val)); |
146 | 2.54k | } |
147 | 738 | char *json = apr_psprintf(r->pool, "{ %s }", apr_array_pstrcat(r->pool, pairs, OIDC_CHAR_COMMA)); |
148 | | |
149 | 738 | 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 | 738 | const char *jmethod = "preserveOnLoad()"; |
156 | 738 | const char *jscript = apr_psprintf( |
157 | 738 | r->pool, |
158 | 738 | " <script type=\"text/javascript\">\n" |
159 | 738 | " function %s {\n" |
160 | 738 | " sessionStorage.setItem('mod_auth_openidc_preserve_post_params', JSON.stringify(%s));\n" |
161 | 738 | " %s" |
162 | 738 | " }\n" |
163 | 738 | " </script>\n", |
164 | 738 | jmethod, json, |
165 | 738 | location |
166 | 738 | ? apr_psprintf(r->pool, "window.location='%s';\n", oidc_util_html_javascript_escape(r->pool, location)) |
167 | 738 | : ""); |
168 | | |
169 | 738 | if (javascript_method) |
170 | 738 | *javascript_method = apr_pstrdup(r->pool, jmethod); |
171 | 738 | if (javascript) |
172 | 738 | *javascript = apr_pstrdup(r->pool, jscript); |
173 | | |
174 | 738 | return TRUE; |
175 | 738 | } |
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 | 586 | char *oidc_response_make_sid_iss_unique(request_rec *r, const char *sid, const char *issuer) { |
220 | 586 | return apr_psprintf(r->pool, "%s@%s", sid, issuer); |
221 | 586 | } |
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 | 421 | const char *original_url, const char *userinfo_jwt) { |
233 | | |
234 | | /* store the user in the session */ |
235 | 421 | session->remote_user = apr_pstrdup(r->pool, remoteUser); |
236 | | |
237 | | /* set the session expiry to the inactivity timeout */ |
238 | 421 | 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 | 421 | oidc_session_set_idtoken_claims(r, session, id_token_jwt->payload.value.json); |
242 | | |
243 | 421 | if (oidc_cfg_store_id_token_get(c)) { |
244 | | /* store the compact serialized representation of the id_token for later reference */ |
245 | 421 | oidc_session_set_idtoken(r, session, id_token); |
246 | 421 | } |
247 | | |
248 | | /* store the issuer in the session (at least needed for session mgmt and token refresh */ |
249 | 421 | 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 | 421 | oidc_session_set_request_state(r, session, state); |
253 | 421 | oidc_session_set_original_url(r, session, original_url); |
254 | | |
255 | 421 | 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 | 421 | } else if (oidc_cfg_provider_check_session_iframe_get(provider) == NULL) { |
264 | 421 | oidc_debug( |
265 | 421 | r, "session management disabled: \"check_session_iframe\" is not set in provider configuration"); |
266 | 421 | } 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 | 421 | oidc_session_set_userinfo_refresh_interval(r, session, |
275 | 421 | oidc_cfg_provider_userinfo_refresh_interval_get(provider)); |
276 | | |
277 | | /* store claims resolved from userinfo endpoint */ |
278 | 421 | oidc_userinfo_store_claims(r, c, session, provider, userinfo_claims, userinfo_jwt); |
279 | | |
280 | | /* see if we have an access_token */ |
281 | 421 | if (access_token != NULL) { |
282 | | /* store the access_token in the session context */ |
283 | 315 | oidc_session_set_access_token(r, session, access_token); |
284 | | /* store the access_token in the session context */ |
285 | 315 | oidc_session_set_access_token_type(r, session, access_token_type); |
286 | | /* store the associated expires_in value */ |
287 | 315 | oidc_session_set_access_token_expires(r, session, expires_in); |
288 | | /* reset the access token refresh timestamp */ |
289 | 315 | oidc_session_set_access_token_last_refresh(r, session, apr_time_now()); |
290 | 315 | } |
291 | | |
292 | | /* see if we have a refresh_token */ |
293 | 421 | 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 | 421 | if (scope != NULL) { |
300 | | /* store the scope in the session context */ |
301 | 5 | oidc_session_set_scope(r, session, scope); |
302 | 5 | } |
303 | | |
304 | | /* store max session duration in the session as a hard cut-off expiry timestamp */ |
305 | 421 | apr_time_t session_expires = |
306 | 421 | (oidc_cfg_provider_session_max_duration_get(provider) == 0) |
307 | 421 | ? oidc_util_apr_time_from_sec(id_token_jwt->payload.exp) |
308 | 421 | : (apr_time_now() + apr_time_from_sec(oidc_cfg_provider_session_max_duration_get(provider))); |
309 | 421 | oidc_session_set_session_expires(r, session, session_expires); |
310 | | |
311 | 421 | oidc_debug(r, "oidc_provider_session_max_duration_get(provider) = %d, session_expires=%" APR_TIME_T_FMT, |
312 | 421 | oidc_cfg_provider_session_max_duration_get(provider), session_expires); |
313 | | |
314 | | /* log message about max session duration */ |
315 | 421 | oidc_log_session_expires(r, "session max lifetime", session_expires); |
316 | | |
317 | | /* store the domain for which this session is valid */ |
318 | 421 | oidc_session_set_cookie_domain(r, session, |
319 | 421 | oidc_cfg_cookie_domain_get(c) |
320 | 421 | ? oidc_cfg_cookie_domain_get(c) |
321 | 421 | : oidc_util_url_cur_host(r, oidc_cfg_x_forwarded_headers_get(c))); |
322 | | |
323 | 421 | char *sid = NULL; |
324 | 421 | char *sub = id_token_jwt->payload.sub; |
325 | 421 | oidc_debug(r, "provider->backchannel_logout_supported=%d", |
326 | 421 | oidc_cfg_provider_backchannel_logout_supported_get(provider)); |
327 | | /* Store sid even without back-channel support because front-channel logout may supply it. */ |
328 | 421 | oidc_jose_get_string(r->pool, id_token_jwt->payload.value.json, OIDC_CLAIM_SID, FALSE, &sid, NULL); |
329 | 421 | if (sid == NULL) |
330 | 420 | sid = sub; |
331 | 421 | 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 | 421 | 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 | 421 | oidc_session_set_session_new(r, session, 1); |
340 | | |
341 | | /* store the session */ |
342 | 421 | return oidc_session_save(r, session, OIDC_SESSION_SAVE_NEW); |
343 | 421 | } |
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 | 3.08k | oidc_proto_state_t **proto_state) { |
350 | | |
351 | 3.08k | oidc_debug(r, "enter"); |
352 | | |
353 | 3.08k | const char *cookieName = oidc_state_cookie_name(r, state); |
354 | | |
355 | | /* clean expired state cookies to avoid pollution */ |
356 | 3.08k | oidc_state_cookies_clean_expired(r, c, cookieName, FALSE); |
357 | | |
358 | | /* get the state cookie value first */ |
359 | 3.08k | const char *cookieValue = oidc_http_get_cookie(r, cookieName); |
360 | 3.08k | if (cookieValue == NULL) { |
361 | 20 | oidc_error(r, "no \"%s\" state cookie found: check domain and samesite cookie settings", cookieName); |
362 | 20 | return FALSE; |
363 | 20 | } |
364 | | |
365 | | /* clear state cookie because we don't need it anymore */ |
366 | 3.06k | oidc_http_set_cookie(r, cookieName, "", 0, OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r)); |
367 | | |
368 | 3.06k | *proto_state = oidc_proto_state_from_cookie(r, c, cookieValue); |
369 | 3.06k | if (*proto_state == NULL) |
370 | 0 | return FALSE; |
371 | | |
372 | 3.06k | const char *nonce = oidc_proto_state_get_nonce(*proto_state); |
373 | | |
374 | | /* calculate the hash of the browser fingerprint concatenated with the nonce */ |
375 | 3.06k | char *calc = oidc_state_browser_fingerprint(r, c, nonce); |
376 | | /* compare the calculated hash with the value provided in the authorization response */ |
377 | 3.06k | 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 | 3.06k | apr_time_t ts = oidc_proto_state_get_timestamp(*proto_state); |
386 | | |
387 | | /* check that the timestamp is not beyond the valid interval */ |
388 | 3.06k | 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 | 3.06k | oidc_proto_state_set_state(*proto_state, state); |
405 | | |
406 | | /* log the restored state object */ |
407 | 3.06k | oidc_debug(r, "restored state: %s", oidc_proto_state_to_string(r, *proto_state)); |
408 | | |
409 | | /* we've made it */ |
410 | 3.06k | return TRUE; |
411 | 3.06k | } |
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 | 3.84k | struct oidc_provider_t **provider, oidc_proto_state_t **proto_state) { |
420 | | |
421 | 3.84k | oidc_debug(r, "enter (state=%s)", state); |
422 | | |
423 | 3.84k | if ((state == NULL) || (_oidc_strcmp(state, "") == 0)) { |
424 | 764 | oidc_error(r, "state parameter is not set"); |
425 | 764 | return FALSE; |
426 | 764 | } |
427 | | |
428 | | /* check the state parameter against what we stored in a cookie */ |
429 | 3.08k | if (oidc_response_proto_state_restore(r, c, state, proto_state) == FALSE) { |
430 | 20 | oidc_error(r, "unable to restore state"); |
431 | 20 | return FALSE; |
432 | 20 | } |
433 | | |
434 | 3.06k | *provider = oidc_get_provider_for_issuer(r, c, oidc_proto_state_get_issuer(*proto_state), FALSE); |
435 | | |
436 | 3.06k | return (*provider != NULL); |
437 | 3.08k | } |
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 | 3.03k | oidc_jwt_t **jwt) { |
445 | | |
446 | 3.03k | apr_byte_t rc = FALSE; |
447 | | |
448 | 3.03k | const char *requested_response_type = oidc_proto_state_get_response_type(proto_state); |
449 | | |
450 | | /* handle the requested response type/mode */ |
451 | 3.03k | if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
452 | 3.03k | OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN_TOKEN)) { |
453 | 109 | rc = oidc_proto_response_code_idtoken_token(r, c, proto_state, provider, params, response_mode, jwt); |
454 | 2.92k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
455 | 2.92k | OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN)) { |
456 | 61 | rc = oidc_proto_response_code_idtoken(r, c, proto_state, provider, params, response_mode, jwt); |
457 | 2.86k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
458 | 2.86k | OIDC_PROTO_RESPONSE_TYPE_CODE_TOKEN)) { |
459 | 61 | rc = oidc_proto_response_code_token(r, c, proto_state, provider, params, response_mode, jwt); |
460 | 2.80k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, OIDC_PROTO_RESPONSE_TYPE_CODE)) { |
461 | 614 | rc = oidc_proto_response_code(r, c, proto_state, provider, params, response_mode, jwt); |
462 | 2.19k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, |
463 | 2.19k | OIDC_PROTO_RESPONSE_TYPE_IDTOKEN_TOKEN)) { |
464 | 529 | rc = oidc_proto_response_idtoken_token(r, c, proto_state, provider, params, response_mode, jwt); |
465 | 1.66k | } else if (oidc_util_spaced_string_equals(r->pool, requested_response_type, OIDC_PROTO_RESPONSE_TYPE_IDTOKEN)) { |
466 | 1.66k | rc = oidc_proto_response_idtoken(r, c, proto_state, provider, params, response_mode, jwt); |
467 | 1.66k | } else { |
468 | 0 | oidc_error(r, "unsupported response type: \"%s\"", requested_response_type); |
469 | 0 | } |
470 | | |
471 | 3.03k | if ((rc == FALSE) && (*jwt != NULL)) { |
472 | 42 | oidc_jwt_destroy(*jwt); |
473 | 42 | *jwt = NULL; |
474 | 42 | } |
475 | | |
476 | 3.03k | return rc; |
477 | 3.03k | } |
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 | 421 | oidc_jwt_t *jwt, const oidc_json_t *userinfo_claims) { |
484 | | |
485 | 421 | const char *issuer = oidc_cfg_provider_issuer_get(provider); |
486 | 421 | char *claim_name = apr_pstrdup(r->pool, oidc_cfg_remote_user_claim_name_get(c)); |
487 | 421 | int n = (int)_oidc_strlen(claim_name); |
488 | 421 | apr_byte_t post_fix_with_issuer = (n > 0) && (claim_name[n - 1] == OIDC_CHAR_AT); |
489 | 421 | if (post_fix_with_issuer == TRUE) { |
490 | 421 | claim_name[n - 1] = '\0'; |
491 | 421 | issuer = (_oidc_strstr(issuer, "https://") == NULL) |
492 | 421 | ? apr_pstrdup(r->pool, issuer) |
493 | 421 | : apr_pstrdup(r->pool, issuer + _oidc_strlen("https://")); |
494 | 421 | } |
495 | | |
496 | | /* extract the username claim (default: "sub") from the id_token payload or user claims */ |
497 | 421 | apr_byte_t rc = FALSE; |
498 | 421 | char *remote_user = NULL; |
499 | 421 | if (userinfo_claims == NULL) { |
500 | 421 | rc = oidc_get_remote_user(r, claim_name, oidc_cfg_remote_user_claim_get(c)->reg_exp, |
501 | 421 | oidc_cfg_remote_user_claim_get(c)->replace, jwt->payload.value.json, |
502 | 421 | &remote_user); |
503 | 421 | } 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 | 421 | 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 | 421 | if (post_fix_with_issuer == TRUE) |
520 | 421 | remote_user = apr_psprintf(r->pool, "%s%s%s", remote_user, OIDC_STR_AT, issuer); |
521 | | |
522 | 421 | r->user = apr_pstrdup(r->pool, remote_user); |
523 | | |
524 | 421 | oidc_debug( |
525 | 421 | r, "set remote_user to \"%s\" based on claim: \"%s\"%s", r->user, oidc_cfg_remote_user_claim_name_get(c), |
526 | 421 | oidc_cfg_remote_user_claim_get(c)->reg_exp |
527 | 421 | ? apr_psprintf(r->pool, " and expression: \"%s\" and replace string: \"%s\"", |
528 | 421 | oidc_cfg_remote_user_claim_get(c)->reg_exp, oidc_cfg_remote_user_claim_get(c)->replace) |
529 | 421 | : ""); |
530 | | |
531 | 421 | return TRUE; |
532 | 421 | } |
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 | 784 | static int oidc_response_handle_state_mismatch(request_rec *r, const oidc_cfg_t *c) { |
540 | 784 | 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 | 784 | 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 | 784 | 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 | 784 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_STATE_MISMATCH); |
558 | | |
559 | 784 | return oidc_util_html_send_error(r, "Invalid Authorization Response", |
560 | 784 | "Could not match the authorization response to an earlier request via " |
561 | 784 | "the state parameter and corresponding state cookie", |
562 | 784 | HTTP_BAD_REQUEST); |
563 | 784 | } |
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 | 356 | const char *original_method) { |
570 | | /* log the successful response */ |
571 | 356 | oidc_debug(r, "session created and stored, returning to original URL: %s, original method: %s", original_url, |
572 | 356 | original_method); |
573 | | |
574 | | /* check whether form post data was preserved; if so restore it */ |
575 | 356 | 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 | 356 | oidc_http_hdr_out_location_set(r, original_url); |
585 | | |
586 | | /* do the actual redirect to the original URL */ |
587 | 356 | return HTTP_MOVED_TEMPORARILY; |
588 | 356 | } |
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 | 3.84k | const char *response_mode) { |
596 | 3.84k | int rc = -1; |
597 | 3.84k | oidc_provider_t *provider = NULL; |
598 | 3.84k | oidc_proto_state_t *proto_state = NULL; |
599 | 3.84k | oidc_jwt_t *id_token = NULL; |
600 | 3.84k | oidc_json_t *userinfo_claims = NULL; |
601 | 3.84k | int expires_in = 0; |
602 | 3.84k | char *userinfo_jwt = NULL; |
603 | 3.84k | const char *s_userinfo_claims = NULL; |
604 | 3.84k | const char *original_url = NULL; |
605 | 3.84k | const char *original_method = NULL; |
606 | 3.84k | const char *prompt = NULL; |
607 | | |
608 | 3.84k | oidc_debug(r, "enter, response_mode=%s", response_mode); |
609 | | |
610 | | /* see if this response came from a browser-back event */ |
611 | 3.84k | 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 | 3.84k | if (oidc_response_match_state(r, c, apr_table_get(params, OIDC_PROTO_STATE), &provider, &proto_state) == |
618 | 3.84k | FALSE) { |
619 | 784 | rc = oidc_response_handle_state_mismatch(r, c); |
620 | 784 | goto end; |
621 | 784 | } |
622 | | |
623 | | /* see if the response is an error response */ |
624 | 3.06k | if (apr_table_get(params, OIDC_PROTO_ERROR) != NULL) { |
625 | 29 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_PROVIDER); |
626 | 29 | rc = oidc_response_authorization_error(r, c, proto_state, apr_table_get(params, OIDC_PROTO_ERROR), |
627 | 29 | apr_table_get(params, OIDC_PROTO_ERROR_DESCRIPTION)); |
628 | 29 | goto end; |
629 | 29 | } |
630 | | |
631 | | /* handle the code, implicit or hybrid flow */ |
632 | 3.03k | if (oidc_response_flows(r, c, proto_state, provider, params, response_mode, &id_token) == FALSE) { |
633 | | /* labeled with the specific validation failure ("signature", "nonce", "exp", ...) when |
634 | | * the failing check recorded one */ |
635 | 2.61k | OIDC_METRICS_COUNTER_INC_VALUE(r, c, OM_AUTHN_RESPONSE_ERROR_PROTOCOL, |
636 | 2.61k | oidc_metrics_error_reason_consume(r)); |
637 | 2.61k | rc = oidc_response_authorization_error(r, c, proto_state, "Error in handling response type.", NULL); |
638 | 2.61k | goto end; |
639 | 2.61k | } |
640 | | |
641 | 421 | if (id_token == NULL) { |
642 | 0 | oidc_error(r, "no id_token was provided"); |
643 | 0 | rc = oidc_response_authorization_error(r, c, proto_state, "No id_token was provided.", NULL); |
644 | 0 | goto end; |
645 | 0 | } |
646 | | |
647 | 421 | expires_in = _oidc_str_to_int(apr_table_get(params, OIDC_PROTO_EXPIRES_IN), -1); |
648 | | |
649 | | /* |
650 | | * optionally resolve additional claims against the userinfo endpoint |
651 | | * parsed claims are not actually used here but need to be parsed anyway for error checking purposes |
652 | | */ |
653 | 421 | s_userinfo_claims = oidc_userinfo_retrieve_claims( |
654 | 421 | r, c, provider, apr_table_get(params, OIDC_PROTO_ACCESS_TOKEN), |
655 | 421 | apr_table_get(params, OIDC_PROTO_TOKEN_TYPE), NULL, id_token->payload.sub, &userinfo_claims, &userinfo_jwt); |
656 | | |
657 | | /* restore the original protected URL that the user was trying to access */ |
658 | 421 | original_url = oidc_proto_state_get_original_url(proto_state); |
659 | 421 | if (original_url != NULL) |
660 | 421 | original_url = apr_pstrdup(r->pool, original_url); |
661 | 421 | original_method = oidc_proto_state_get_original_method(proto_state); |
662 | 421 | if (original_method != NULL) |
663 | 421 | original_method = apr_pstrdup(r->pool, original_method); |
664 | 421 | prompt = oidc_proto_state_get_prompt(proto_state); |
665 | | |
666 | | /* set the user */ |
667 | 421 | if (oidc_response_set_request_user(r, c, provider, id_token, userinfo_claims) == FALSE) { |
668 | 0 | oidc_error(r, "remote user could not be set"); |
669 | 0 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_REMOTE_USER); |
670 | 0 | rc = oidc_response_authorization_error( |
671 | 0 | r, c, proto_state, "Remote user could not be set: contact the website administrator", NULL); |
672 | 0 | goto end; |
673 | 0 | } |
674 | | |
675 | 421 | oidc_debug(r, "set remote_user to \"%s\" in new session \"%s\"", r->user, |
676 | 421 | oidc_session_id_fingerprint(r, session->uuid)); |
677 | | |
678 | | /* For prompt=none, reject a different existing remote user. With no prior user, establish a new session. */ |
679 | 421 | if ((prompt != NULL) && (_oidc_strcmp(prompt, OIDC_PROTO_PROMPT_NONE) == 0) && (session->remote_user != NULL) && |
680 | 0 | (_oidc_strcmp(session->remote_user, r->user) != 0)) { |
681 | 0 | oidc_warn(r, "user set from new id_token is different from current one"); |
682 | 0 | rc = oidc_response_authorization_error(r, c, proto_state, "User changed!", NULL); |
683 | 0 | goto end; |
684 | 0 | } |
685 | | |
686 | | /* Reset after checks that need the old session and before storing the new authentication. */ |
687 | 421 | oidc_session_reset(r, c, session); |
688 | | |
689 | | /* Persist per-path settings only when session management may reuse them for silent reauthentication. */ |
690 | 421 | if (oidc_cfg_provider_check_session_iframe_get(provider) != NULL) { |
691 | 0 | oidc_session_set_path_auth_request_params(r, session, |
692 | 0 | oidc_proto_state_get_auth_request_params(proto_state)); |
693 | 0 | oidc_session_set_path_scope(r, session, oidc_proto_state_get_path_scope(proto_state)); |
694 | 0 | } |
695 | | |
696 | | /* store resolved information in the session */ |
697 | 421 | if (oidc_response_save_in_session( |
698 | 421 | r, c, session, provider, r->user, apr_table_get(params, OIDC_PROTO_ID_TOKEN), id_token, |
699 | 421 | s_userinfo_claims, userinfo_claims, apr_table_get(params, OIDC_PROTO_ACCESS_TOKEN), |
700 | 421 | apr_table_get(params, OIDC_PROTO_TOKEN_TYPE), expires_in, |
701 | 421 | apr_table_get(params, OIDC_PROTO_REFRESH_TOKEN), apr_table_get(params, OIDC_PROTO_SCOPE), |
702 | 421 | apr_table_get(params, OIDC_PROTO_SESSION_STATE), apr_table_get(params, OIDC_PROTO_STATE), original_url, |
703 | 421 | userinfo_jwt) == FALSE) { |
704 | 65 | rc = HTTP_INTERNAL_SERVER_ERROR; |
705 | 65 | goto end; |
706 | 65 | } |
707 | | |
708 | | /* check that we've actually authenticated a user; functions as error handling for oidc_get_remote_user */ |
709 | 356 | if (r->user == NULL) { |
710 | 0 | OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_REMOTE_USER); |
711 | 0 | rc = HTTP_UNAUTHORIZED; |
712 | 0 | goto end; |
713 | 0 | } |
714 | | |
715 | 356 | rc = oidc_response_finish_success(r, c, original_url, original_method); |
716 | | |
717 | 3.84k | end: |
718 | | |
719 | 3.84k | if (proto_state) |
720 | 3.06k | oidc_proto_state_destroy(proto_state); |
721 | 3.84k | if (id_token) |
722 | 421 | oidc_jwt_destroy(id_token); |
723 | 3.84k | if (userinfo_claims) |
724 | 0 | oidc_json_decref(userinfo_claims); |
725 | | |
726 | 3.84k | return rc; |
727 | 356 | } |
728 | | |
729 | | /* specification-defined parameters that must not be repeated in an authorization response */ |
730 | | static const char *const OIDC_RESPONSE_NO_REPEAT[] = {OIDC_PROTO_STATE, OIDC_PROTO_CODE, OIDC_PROTO_ACCESS_TOKEN, |
731 | | OIDC_PROTO_ID_TOKEN, NULL}; |
732 | | |
733 | | /* |
734 | | * handle an OpenID Connect Authorization Response using the POST (+fragment->POST) response_mode |
735 | | */ |
736 | 1.40k | int oidc_response_authorization_post(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) { |
737 | | |
738 | 1.40k | oidc_debug(r, "enter"); |
739 | | |
740 | | /* initialize local variables */ |
741 | 1.40k | const char *response_mode = NULL; |
742 | | |
743 | | /* read the parameters that are POST-ed to us */ |
744 | 1.40k | apr_table_t *params = apr_table_make(r->pool, 8); |
745 | 1.40k | if (oidc_util_read_post_params_reject_dup(r, params, FALSE, NULL, OIDC_RESPONSE_NO_REPEAT) == FALSE) { |
746 | 22 | oidc_error(r, "something went wrong when reading the POST parameters"); |
747 | 22 | return HTTP_BAD_REQUEST; |
748 | 22 | } |
749 | | |
750 | | /* see if we've got any POST-ed data at all */ |
751 | 1.38k | if ((apr_table_elts(params)->nelts < 1) || |
752 | 1.38k | ((apr_table_elts(params)->nelts == 1) && apr_table_get(params, OIDC_PROTO_RESPONSE_MODE) && |
753 | 75 | (_oidc_strcmp(apr_table_get(params, OIDC_PROTO_RESPONSE_MODE), OIDC_PROTO_RESPONSE_MODE_FRAGMENT) == 0))) { |
754 | 6 | return oidc_util_html_send_error( |
755 | 6 | r, "Invalid Request", |
756 | 6 | "You've hit an OpenID Connect Redirect URI with no parameters, this is an invalid request; you " |
757 | 6 | "should not open this URL in your browser directly, or have the server administrator use a " |
758 | 6 | "different " OIDCRedirectURI " setting.", |
759 | 6 | HTTP_INTERNAL_SERVER_ERROR); |
760 | 6 | } |
761 | | |
762 | | /* get the parameters */ |
763 | 1.38k | response_mode = apr_table_get(params, OIDC_PROTO_RESPONSE_MODE); |
764 | | |
765 | | /* do the actual implicit work */ |
766 | 1.38k | return oidc_response_process(r, c, session, params, |
767 | 1.38k | response_mode ? response_mode : OIDC_PROTO_RESPONSE_MODE_FORM_POST); |
768 | 1.38k | } |
769 | | |
770 | | /* |
771 | | * handle an OpenID Connect Authorization Response using the redirect response_mode |
772 | | */ |
773 | 2.50k | int oidc_response_authorization_redirect(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) { |
774 | | |
775 | 2.50k | oidc_debug(r, "enter"); |
776 | | |
777 | | /* read the parameters from the query string */ |
778 | 2.50k | apr_table_t *params = apr_table_make(r->pool, 8); |
779 | 2.50k | if (oidc_util_read_form_encoded_params_reject_dup(r, params, r->args, OIDC_RESPONSE_NO_REPEAT) == FALSE) |
780 | 31 | return HTTP_BAD_REQUEST; |
781 | | |
782 | | /* do the actual work */ |
783 | 2.46k | return oidc_response_process(r, c, session, params, OIDC_PROTO_RESPONSE_MODE_QUERY); |
784 | 2.50k | } |