/src/mod_auth_openidc/src/metadata/client.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 | | * Copyright (C) 2013-2017 Ping Identity Corporation |
23 | | * All rights reserved. |
24 | | * |
25 | | * Client metadata: validation, parsing, on-disk read, and Dynamic Client |
26 | | * Registration with the OP. |
27 | | * |
28 | | * @Author: Hans Zandbelt - hans.zandbelt@openidc.com |
29 | | */ |
30 | | |
31 | | #include "metadata/internal.h" |
32 | | #include "util/util_cfg.h" |
33 | | |
34 | | #include "cfg/dir.h" |
35 | | #include "http.h" |
36 | | #include "metrics.h" |
37 | | #include "mod_auth_openidc.h" |
38 | | #include "proto/proto.h" |
39 | | #include "util/util.h" |
40 | | |
41 | | #include <apr_strings.h> |
42 | | |
43 | | /* |
44 | | * check to see if dynamically registered JSON client metadata is valid and has not expired |
45 | | */ |
46 | 0 | static apr_byte_t oidc_metadata_client_is_valid(request_rec *r, const oidc_json_t *j_client, const char *issuer) { |
47 | |
|
48 | 0 | char *str; |
49 | | |
50 | | /* get a handle to the client_id we need to use for this provider */ |
51 | 0 | str = NULL; |
52 | 0 | oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_ID, &str, NULL); |
53 | 0 | if (str == NULL) { |
54 | 0 | oidc_error(r, "client (%s) JSON metadata did not contain a \"" OIDC_METADATA_CLIENT_ID "\" string", |
55 | 0 | issuer); |
56 | 0 | return FALSE; |
57 | 0 | } |
58 | | |
59 | | /* get a handle to the client_secret we need to use for this provider */ |
60 | 0 | str = NULL; |
61 | 0 | oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_SECRET, &str, NULL); |
62 | 0 | if (str == NULL) { |
63 | 0 | oidc_warn(r, "client (%s) JSON metadata did not contain a \"" OIDC_METADATA_CLIENT_SECRET "\" string", |
64 | 0 | issuer); |
65 | 0 | } |
66 | | |
67 | | /* the expiry timestamp from the JSON object */ |
68 | 0 | const oidc_json_t *expires_at = oidc_json_object_get(j_client, OIDC_METADATA_CLIENT_SECRET_EXPIRES_AT); |
69 | 0 | if ((expires_at == NULL) || (!oidc_json_is_integer(expires_at))) { |
70 | 0 | oidc_debug( |
71 | 0 | r, "client (%s) metadata did not contain a \"" OIDC_METADATA_CLIENT_SECRET_EXPIRES_AT "\" setting", |
72 | 0 | issuer); |
73 | | /* assume that it never expires */ |
74 | 0 | return TRUE; |
75 | 0 | } |
76 | | |
77 | | /* see if it is unrestricted */ |
78 | 0 | if (oidc_json_integer_value(expires_at) == 0) { |
79 | 0 | oidc_debug(r, "client (%s) metadata never expires (" OIDC_METADATA_CLIENT_SECRET_EXPIRES_AT "=0)", |
80 | 0 | issuer); |
81 | 0 | return TRUE; |
82 | 0 | } |
83 | | |
84 | | /* check if the value >= now */ |
85 | 0 | if (apr_time_sec(apr_time_now()) > oidc_json_integer_value(expires_at)) { |
86 | 0 | oidc_warn(r, "client (%s) secret expired", issuer); |
87 | 0 | return FALSE; |
88 | 0 | } |
89 | | |
90 | 0 | oidc_debug(r, "client (%s) metadata is valid", issuer); |
91 | |
|
92 | 0 | return TRUE; |
93 | 0 | } |
94 | | |
95 | | /* |
96 | | * register the client with the OP using Dynamic Client Registration |
97 | | */ |
98 | | apr_byte_t oidc_metadata_client_register(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider, |
99 | 0 | oidc_json_t **j_client, char **response) { |
100 | | |
101 | | /* assemble the JSON registration request */ |
102 | 0 | oidc_json_t *data = oidc_json_object(); |
103 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_CLIENT_NAME, |
104 | 0 | oidc_json_string(oidc_cfg_provider_client_name_get(provider))); |
105 | |
|
106 | 0 | oidc_json_t *redirect_uris = oidc_json_array(); |
107 | 0 | oidc_json_array_append_new(redirect_uris, oidc_json_string(oidc_util_url_redirect_uri(r, cfg))); |
108 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_REDIRECT_URIS, redirect_uris); |
109 | |
|
110 | 0 | oidc_json_t *response_types = oidc_json_array(); |
111 | 0 | apr_array_header_t *flows = oidc_proto_supported_flows(r->pool); |
112 | 0 | for (int i = 0; i < flows->nelts; i++) |
113 | 0 | oidc_json_array_append_new(response_types, oidc_json_string(APR_ARRAY_IDX(flows, i, const char *))); |
114 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_RESPONSE_TYPES, response_types); |
115 | |
|
116 | 0 | oidc_json_t *grant_types = oidc_json_array(); |
117 | 0 | oidc_json_array_append_new(grant_types, oidc_json_string("authorization_code")); |
118 | 0 | oidc_json_array_append_new(grant_types, oidc_json_string("implicit")); |
119 | 0 | oidc_json_array_append_new(grant_types, oidc_json_string("refresh_token")); |
120 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_GRANT_TYPES, grant_types); |
121 | |
|
122 | 0 | if (oidc_cfg_provider_token_endpoint_auth_get(provider) != NULL) |
123 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_TOKEN_ENDPOINT_AUTH_METHOD, |
124 | 0 | oidc_json_string(oidc_cfg_provider_token_endpoint_auth_get(provider))); |
125 | | |
126 | | /* Request certificate-bound tokens when the provider's resolved RFC 8705 mode requires them. */ |
127 | 0 | if ((oidc_cfg_provider_cert_bound_tokens_get(provider) == OIDC_CERT_BOUND_TOKENS_ON) || |
128 | 0 | (oidc_cfg_endpoint_auth_is_mtls(oidc_cfg_provider_token_endpoint_auth_get(provider)) == TRUE)) |
129 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS, |
130 | 0 | oidc_json_boolean(1)); |
131 | |
|
132 | 0 | if (oidc_cfg_provider_client_contact_get(provider) != NULL) { |
133 | 0 | oidc_json_t *contacts = oidc_json_array(); |
134 | 0 | oidc_json_array_append_new(contacts, oidc_json_string(oidc_cfg_provider_client_contact_get(provider))); |
135 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_CONTACTS, contacts); |
136 | 0 | } |
137 | |
|
138 | 0 | if (oidc_cfg_provider_client_jwks_uri_get(provider)) { |
139 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_JWKS_URI, |
140 | 0 | oidc_json_string(oidc_cfg_provider_client_jwks_uri_get(provider))); |
141 | 0 | } else if (oidc_cfg_public_keys_get(cfg) != NULL) { |
142 | 0 | oidc_json_object_set_new( |
143 | 0 | data, OIDC_METADATA_JWKS_URI, |
144 | 0 | oidc_json_string(apr_psprintf(r->pool, "%s?%s=rsa", oidc_util_url_redirect_uri(r, cfg), |
145 | 0 | OIDC_REDIRECT_URI_REQUEST_JWKS))); |
146 | 0 | } |
147 | |
|
148 | 0 | if (oidc_cfg_provider_id_token_signed_response_alg_get(provider) != NULL) { |
149 | 0 | oidc_json_object_set_new( |
150 | 0 | data, OIDC_METADATA_ID_TOKEN_SIGNED_RESPONSE_ALG, |
151 | 0 | oidc_json_string(oidc_cfg_provider_id_token_signed_response_alg_get(provider))); |
152 | 0 | } |
153 | 0 | if (oidc_cfg_provider_id_token_encrypted_response_alg_get(provider) != NULL) { |
154 | 0 | oidc_json_object_set_new( |
155 | 0 | data, OIDC_METADATA_ID_TOKEN_ENCRYPTED_RESPONSE_ALG, |
156 | 0 | oidc_json_string(oidc_cfg_provider_id_token_encrypted_response_alg_get(provider))); |
157 | 0 | } |
158 | 0 | if (oidc_cfg_provider_id_token_encrypted_response_enc_get(provider) != NULL) { |
159 | 0 | oidc_json_object_set_new( |
160 | 0 | data, OIDC_METADATA_ID_TOKEN_ENCRYPTED_RESPONSE_ENC, |
161 | 0 | oidc_json_string(oidc_cfg_provider_id_token_encrypted_response_enc_get(provider))); |
162 | 0 | } |
163 | |
|
164 | 0 | if (oidc_cfg_provider_userinfo_signed_response_alg_get(provider) != NULL) { |
165 | 0 | oidc_json_object_set_new( |
166 | 0 | data, OIDC_METADATA_USERINFO_SIGNED_RESPONSE_ALG, |
167 | 0 | oidc_json_string(oidc_cfg_provider_userinfo_signed_response_alg_get(provider))); |
168 | 0 | } |
169 | 0 | if (oidc_cfg_provider_userinfo_encrypted_response_alg_get(provider) != NULL) { |
170 | 0 | oidc_json_object_set_new( |
171 | 0 | data, OIDC_METADATA_USERINFO_ENCRYPTED_RESPONSE_ALG, |
172 | 0 | oidc_json_string(oidc_cfg_provider_userinfo_encrypted_response_alg_get(provider))); |
173 | 0 | } |
174 | 0 | if (oidc_cfg_provider_userinfo_encrypted_response_enc_get(provider) != NULL) { |
175 | 0 | oidc_json_object_set_new( |
176 | 0 | data, OIDC_METADATA_USERINFO_ENCRYPTED_RESPONSE_ENC, |
177 | 0 | oidc_json_string(oidc_cfg_provider_userinfo_encrypted_response_enc_get(provider))); |
178 | 0 | } |
179 | |
|
180 | 0 | if (oidc_cfg_provider_request_object_get(provider) != NULL) { |
181 | 0 | oidc_json_t *request_object_config = NULL; |
182 | 0 | if (oidc_json_decode_object(r, oidc_cfg_provider_request_object_get(provider), |
183 | 0 | &request_object_config) == TRUE) { |
184 | 0 | const oidc_json_t *crypto = |
185 | 0 | oidc_json_object_get(request_object_config, OIDC_REQUEST_OBJECT_CRYPTO); |
186 | 0 | char *alg = "none"; |
187 | 0 | oidc_json_object_get_string(r->pool, crypto, OIDC_REQUEST_OBJECT_CRYPTO_SIGN_ALG, &alg, "none"); |
188 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_REQUEST_OBJECT_SIGNING_ALG, oidc_json_string(alg)); |
189 | 0 | oidc_json_decref(request_object_config); |
190 | 0 | } |
191 | 0 | } |
192 | |
|
193 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_INITIATE_LOGIN_URI, |
194 | 0 | oidc_json_string(oidc_util_url_redirect_uri(r, cfg))); |
195 | |
|
196 | 0 | oidc_json_object_set_new( |
197 | 0 | data, OIDC_METADATA_FRONTCHANNEL_LOGOUT_URI, |
198 | 0 | oidc_json_string(apr_psprintf(r->pool, "%s?%s=%s", oidc_util_url_redirect_uri(r, cfg), |
199 | 0 | OIDC_REDIRECT_URI_REQUEST_LOGOUT, OIDC_GET_STYLE_LOGOUT_PARAM_VALUE))); |
200 | | |
201 | | // NB: "backchannel_logout_session_required" is not advertised in the registration |
202 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_BACKCHANNEL_LOGOUT_URI, |
203 | 0 | oidc_json_string(apr_psprintf(r->pool, "%s?%s=%s", oidc_util_url_redirect_uri(r, cfg), |
204 | 0 | OIDC_REDIRECT_URI_REQUEST_LOGOUT, |
205 | 0 | OIDC_BACKCHANNEL_STYLE_LOGOUT_PARAM_VALUE))); |
206 | |
|
207 | 0 | if (oidc_cfg_default_slo_url_get(cfg) != NULL) { |
208 | 0 | oidc_json_t *post_logout_redirect_uris = oidc_json_array(); |
209 | 0 | oidc_json_array_append_new(post_logout_redirect_uris, oidc_json_string(oidc_util_url_abs( |
210 | 0 | r, cfg, oidc_cfg_default_slo_url_get(cfg)))); |
211 | 0 | oidc_json_object_set_new(data, OIDC_METADATA_POST_LOGOUT_REDIRECT_URIS, post_logout_redirect_uris); |
212 | 0 | } |
213 | | |
214 | | /* add any custom JSON in to the registration request */ |
215 | 0 | if (oidc_cfg_provider_registration_endpoint_json_get(provider) != NULL) { |
216 | 0 | oidc_json_t *json = NULL; |
217 | 0 | if (oidc_json_decode_object(r, oidc_cfg_provider_registration_endpoint_json_get(provider), &json) == |
218 | 0 | FALSE) { |
219 | 0 | oidc_json_decref(data); |
220 | 0 | return FALSE; |
221 | 0 | } |
222 | 0 | oidc_json_merge(r, json, data); |
223 | 0 | oidc_json_decref(json); |
224 | 0 | } |
225 | | |
226 | | /* dynamically register the client with the specified parameters |
227 | | * NB: the TLS client certificate is presented here as it is at the other endpoints, since the |
228 | | * registration endpoint may be an RFC 8705 section 5 "mtls_endpoint_aliases" one */ |
229 | 0 | if (oidc_http_post_json(r, oidc_cfg_provider_registration_endpoint_url_get(provider), data, NULL, |
230 | 0 | oidc_cfg_provider_registration_token_get(provider), NULL, |
231 | 0 | oidc_cfg_provider_ssl_validate_server_get(provider), response, NULL, NULL, |
232 | 0 | oidc_cfg_http_timeout_short_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), |
233 | 0 | oidc_cfg_dir_pass_cookies_get(r), |
234 | 0 | oidc_cfg_provider_token_endpoint_tls_client_cert_get(provider), |
235 | 0 | oidc_cfg_provider_token_endpoint_tls_client_key_get(provider), |
236 | 0 | oidc_cfg_provider_token_endpoint_tls_client_key_pwd_get(provider)) == FALSE) { |
237 | 0 | OIDC_METRICS_COUNTER_INC(r, cfg, OM_PROVIDER_REGISTRATION_ERROR); |
238 | 0 | oidc_json_decref(data); |
239 | 0 | return FALSE; |
240 | 0 | } |
241 | 0 | oidc_json_decref(data); |
242 | | |
243 | | /* decode and see if it is not an error response somehow */ |
244 | 0 | if (oidc_json_decode_and_check_error(r, *response, j_client) == FALSE) { |
245 | 0 | OIDC_METRICS_COUNTER_INC(r, cfg, OM_PROVIDER_REGISTRATION_ERROR); |
246 | 0 | oidc_error(r, "JSON parsing of dynamic client registration response failed"); |
247 | 0 | return FALSE; |
248 | 0 | } |
249 | | |
250 | 0 | return TRUE; |
251 | 0 | } |
252 | | |
253 | | /* |
254 | | * see if we have client metadata and check its validity |
255 | | * if not, use OpenID Connect Client Registration to get it, check it and store it |
256 | | */ |
257 | | apr_byte_t oidc_metadata_client_get(request_rec *r, oidc_cfg_t *cfg, const char *issuer, |
258 | 0 | const oidc_provider_t *provider, oidc_json_t **j_client) { |
259 | | |
260 | | /* get the full file path to the client metadata for this issuer */ |
261 | 0 | const char *client_path = oidc_metadata_client_file_path(r, issuer); |
262 | | |
263 | | /* if we have valid client metadata already, return it */ |
264 | 0 | if (oidc_metadata_file_read_json(r, client_path, j_client) == TRUE) { |
265 | 0 | if (oidc_metadata_client_is_valid(r, *j_client, issuer) == TRUE) |
266 | 0 | return TRUE; |
267 | | /* stale/invalid on-disk metadata: release it before re-registering overwrites *j_client */ |
268 | 0 | oidc_json_decref(*j_client); |
269 | 0 | *j_client = NULL; |
270 | 0 | } |
271 | | |
272 | | /* at this point we have no valid client metadata, see if there's a registration endpoint for this provider */ |
273 | 0 | if (oidc_cfg_provider_registration_endpoint_url_get(provider) == NULL) { |
274 | 0 | oidc_error(r, |
275 | 0 | "no (valid) client metadata exists for provider (%s) and provider JSON object did not " |
276 | 0 | "contain a (valid) \"" OIDC_METADATA_REGISTRATION_ENDPOINT "\" string", |
277 | 0 | issuer); |
278 | 0 | return FALSE; |
279 | 0 | } |
280 | | |
281 | | /* try and get client metadata by registering the client at the registration endpoint */ |
282 | 0 | char *response = NULL; |
283 | 0 | if (oidc_metadata_client_register(r, cfg, provider, j_client, &response) == FALSE) |
284 | 0 | return FALSE; |
285 | | |
286 | | /* check to see if it is valid metadata */ |
287 | 0 | if (oidc_metadata_client_is_valid(r, *j_client, issuer) == FALSE) |
288 | 0 | return FALSE; |
289 | | |
290 | | /* since it is valid, write the obtained client metadata file */ |
291 | 0 | if (oidc_util_file_write(r, client_path, response) == FALSE) |
292 | 0 | return FALSE; |
293 | | |
294 | 0 | return TRUE; |
295 | 0 | } |
296 | | |
297 | | /* |
298 | | * override the provider token endpoint auth method when the client metadata specifies one |
299 | | */ |
300 | | static void oidc_metadata_client_parse_token_endpoint_auth(request_rec *r, const oidc_cfg_t *cfg, |
301 | 5.08k | const oidc_json_t *j_client, oidc_provider_t *provider) { |
302 | | |
303 | 5.08k | char *value = NULL; |
304 | 5.08k | oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_TOKEN_ENDPOINT_AUTH_METHOD, &value, NULL); |
305 | 5.08k | if (value == NULL) |
306 | 5.07k | return; |
307 | | |
308 | 3 | const char *rv = oidc_cfg_provider_token_endpoint_auth_set(r->pool, cfg, provider, value); |
309 | 3 | if (rv != NULL) |
310 | 3 | oidc_error(r, "oidc_provider_token_endpoint_auth_set: %s", value); |
311 | 3 | } |
312 | | |
313 | | /* |
314 | | * determine the provider response_type when not already set by .conf: default from the global config, |
315 | | * then fall back to the first entry of the client metadata "response_types" array if the configured |
316 | | * one is not advertised as supported |
317 | | */ |
318 | | static void oidc_metadata_client_parse_response_type(request_rec *r, oidc_cfg_t *cfg, const oidc_json_t *j_client, |
319 | 5.08k | oidc_provider_t *provider) { |
320 | | |
321 | 5.08k | const char *value = NULL; |
322 | | |
323 | 5.08k | if (oidc_cfg_provider_response_type_is_set(provider)) |
324 | 7 | return; |
325 | | |
326 | 5.07k | oidc_cfg_provider_response_type_set(r->pool, provider, |
327 | 5.07k | oidc_cfg_provider_response_type_get(oidc_cfg_provider_get(cfg))); |
328 | | |
329 | | // "response_types" is an array in the client metadata as by spec |
330 | 5.07k | const oidc_json_t *j_response_types = oidc_json_object_get(j_client, OIDC_METADATA_RESPONSE_TYPES); |
331 | 5.07k | if ((j_response_types == NULL) || (!oidc_json_is_array(j_response_types))) |
332 | 4.99k | return; |
333 | | |
334 | | // if there's an array we'll prefer the configured response_type if supported |
335 | 80 | if (oidc_json_array_has_value(r, j_response_types, oidc_cfg_provider_response_type_get(provider)) == TRUE) |
336 | 13 | return; |
337 | | |
338 | | // if the configured response_type is not supported, we'll fallback to the first one that is listed |
339 | 67 | const oidc_json_t *j_response_type = oidc_json_array_get(j_response_types, 0); |
340 | 67 | if (oidc_json_is_string(j_response_type)) { |
341 | 39 | value = apr_pstrdup(r->pool, oidc_json_string_value(j_response_type)); |
342 | 39 | OIDC_METADATA_PROVIDER_SET(response_type, value); |
343 | 39 | } |
344 | 67 | } |
345 | | |
346 | | /* |
347 | | * parse the JSON client metadata in to a oidc_provider_t struct |
348 | | */ |
349 | | apr_byte_t oidc_metadata_client_parse(request_rec *r, oidc_cfg_t *cfg, const oidc_json_t *j_client, |
350 | 5.08k | oidc_provider_t *provider) { |
351 | | |
352 | 5.08k | char *value = NULL; |
353 | | |
354 | | /* get a handle to the client_id we need to use for this provider */ |
355 | 5.08k | oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_ID, &value, NULL); |
356 | 5.08k | OIDC_METADATA_PROVIDER_SET(client_id, value); |
357 | | |
358 | | /* get a handle to the client_secret we need to use for this provider */ |
359 | 5.08k | oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_CLIENT_SECRET, &value, NULL); |
360 | 5.08k | OIDC_METADATA_PROVIDER_SET(client_secret, value); |
361 | | |
362 | | /* see if the token endpoint auth method defined in the client metadata overrides the provider one */ |
363 | 5.08k | oidc_metadata_client_parse_token_endpoint_auth(r, cfg, j_client, provider); |
364 | | |
365 | | /* determine the response type if not set by .conf */ |
366 | 5.08k | oidc_metadata_client_parse_response_type(r, cfg, j_client, provider); |
367 | | |
368 | 5.08k | oidc_json_object_get_string(r->pool, j_client, OIDC_METADATA_ID_TOKEN_SIGNED_RESPONSE_ALG, &value, |
369 | 5.08k | oidc_cfg_provider_id_token_signed_response_alg_get(oidc_cfg_provider_get(cfg))); |
370 | 5.08k | OIDC_METADATA_PROVIDER_SET(id_token_signed_response_alg, value); |
371 | | |
372 | | // NB: id_token_encrypted_response_alg and the other encryption-related client metadata are not parsed here |
373 | | |
374 | 5.08k | return TRUE; |
375 | 5.08k | } |