/src/mod_auth_openidc/src/proto/auth.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/parse.h" |
44 | | #include "mod_auth_openidc.h" |
45 | | #include "proto/proto.h" |
46 | | #include "util/util.h" |
47 | | |
48 | | /* |
49 | | * setup for an endpoint call without authentication |
50 | | */ |
51 | 0 | static apr_byte_t oidc_proto_endpoint_auth_none(const char *client_id, apr_table_t *params) { |
52 | 0 | apr_table_set(params, OIDC_PROTO_CLIENT_ID, client_id); |
53 | 0 | return TRUE; |
54 | 0 | } |
55 | | |
56 | | /* |
57 | | * setup for an endpoint call with OIDC client_secret_basic authentication |
58 | | */ |
59 | | static apr_byte_t oidc_proto_endpoint_client_secret_basic(request_rec *r, const char *client_id, |
60 | 246 | const char *client_secret, char **basic_auth_str) { |
61 | 246 | oidc_debug(r, "enter"); |
62 | 246 | if (client_secret == NULL) { |
63 | 0 | oidc_error(r, "no client secret is configured"); |
64 | 0 | return FALSE; |
65 | 0 | } |
66 | 246 | *basic_auth_str = |
67 | 246 | apr_psprintf(r->pool, "%s:%s", oidc_http_url_encode(r, client_id), oidc_http_url_encode(r, client_secret)); |
68 | | |
69 | 246 | return TRUE; |
70 | 246 | } |
71 | | |
72 | | /* |
73 | | * setup for an endpoint call with OIDC client_secret_post authentication |
74 | | */ |
75 | | static apr_byte_t oidc_proto_endpoint_client_secret_post(request_rec *r, const char *client_id, |
76 | 0 | const char *client_secret, apr_table_t *params) { |
77 | 0 | oidc_debug(r, "enter"); |
78 | 0 | if (client_secret == NULL) { |
79 | 0 | oidc_error(r, "no client secret is configured"); |
80 | 0 | return FALSE; |
81 | 0 | } |
82 | 0 | apr_table_set(params, OIDC_PROTO_CLIENT_ID, client_id); |
83 | 0 | apr_table_set(params, OIDC_PROTO_CLIENT_SECRET, client_secret); |
84 | 0 | return TRUE; |
85 | 0 | } |
86 | | |
87 | | /* |
88 | | * helper function to create a JWT assertion for endpoint authentication |
89 | | */ |
90 | | static apr_byte_t oidc_proto_jwt_create(request_rec *r, const char *client_id, oidc_proto_endpoint_auth_aud_t audience, |
91 | 0 | oidc_jwt_t **out) { |
92 | |
|
93 | 0 | *out = oidc_jwt_new(r->pool, TRUE, TRUE); |
94 | 0 | oidc_jwt_t *jwt = *out; |
95 | | |
96 | | /* RFC 10070 section 4: type the assertion explicitly, but only when its audience is the issuer identifier; |
97 | | * the "typ" signals that the client follows the tightened audience requirements, so a pre-RFC 10070 |
98 | | * audience such as the token endpoint URL must not carry it */ |
99 | 0 | if (audience.is_issuer) |
100 | 0 | oidc_json_object_set_new(jwt->header.value.json, OIDC_CLAIM_TYP, |
101 | 0 | oidc_json_string(OIDC_PROTO_CLIENT_ASSERTION_JWT_TYP)); |
102 | |
|
103 | 0 | oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_ISS, oidc_json_string(client_id)); |
104 | 0 | oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_SUB, oidc_json_string(client_id)); |
105 | 0 | oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_AUD, oidc_json_string(audience.value)); |
106 | 0 | oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_JTI, oidc_json_string(oidc_proto_jti_gen(r))); |
107 | 0 | oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_EXP, |
108 | 0 | oidc_json_integer(apr_time_sec(apr_time_now()) + 60)); |
109 | 0 | oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_IAT, |
110 | 0 | oidc_json_integer(apr_time_sec(apr_time_now()))); |
111 | |
|
112 | 0 | return TRUE; |
113 | 0 | } |
114 | | |
115 | | /* |
116 | | * helper function to add a JWT assertion to the HTTP request as endpoint authentication |
117 | | */ |
118 | | static apr_byte_t oidc_proto_jwt_sign_and_add(request_rec *r, apr_table_t *params, oidc_jwt_t *jwt, |
119 | 0 | const oidc_jwk_t *jwk) { |
120 | 0 | char *cser = NULL; |
121 | |
|
122 | 0 | if (oidc_proto_jwt_sign_and_serialize(r, jwk, jwt, &cser) == FALSE) |
123 | 0 | return FALSE; |
124 | | |
125 | 0 | apr_table_setn(params, OIDC_PROTO_CLIENT_ASSERTION_TYPE, OIDC_PROTO_CLIENT_ASSERTION_TYPE_JWT_BEARER); |
126 | 0 | apr_table_set(params, OIDC_PROTO_CLIENT_ASSERTION, cser); |
127 | |
|
128 | 0 | return TRUE; |
129 | 0 | } |
130 | | |
131 | 0 | #define OIDC_PROTO_JWT_ASSERTION_SYMMETRIC_ALG OIDC_JOSE_HDR_ALG_HS256 |
132 | | |
133 | | /* |
134 | | * create a JWT assertion signed with the client secret and add it to the HTTP request as endpoint authentication |
135 | | */ |
136 | | static apr_byte_t oidc_proto_endpoint_auth_client_secret_jwt(request_rec *r, const char *client_id, |
137 | | const char *client_secret, |
138 | | oidc_proto_endpoint_auth_aud_t audience, |
139 | 0 | apr_table_t *params) { |
140 | 0 | oidc_jwt_t *jwt = NULL; |
141 | 0 | oidc_jose_error_t err; |
142 | |
|
143 | 0 | oidc_debug(r, "enter"); |
144 | |
|
145 | 0 | if (oidc_proto_jwt_create(r, client_id, audience, &jwt) == FALSE) |
146 | 0 | return FALSE; |
147 | | |
148 | 0 | oidc_jwk_t *jwk = oidc_jwk_create_symmetric_key(r->pool, NULL, (const unsigned char *)client_secret, |
149 | 0 | (unsigned int)_oidc_strlen(client_secret), FALSE, &err); |
150 | 0 | if (jwk == NULL) { |
151 | 0 | oidc_error(r, "parsing of client secret into JWK failed: %s", oidc_jose_e2s(r->pool, err)); |
152 | 0 | oidc_jwt_destroy(jwt); |
153 | 0 | return FALSE; |
154 | 0 | } |
155 | | |
156 | 0 | jwt->header.alg = apr_pstrdup(r->pool, OIDC_PROTO_JWT_ASSERTION_SYMMETRIC_ALG); |
157 | |
|
158 | 0 | oidc_proto_jwt_sign_and_add(r, params, jwt, jwk); |
159 | |
|
160 | 0 | oidc_jwt_destroy(jwt); |
161 | 0 | oidc_jwk_destroy(jwk); |
162 | |
|
163 | 0 | return TRUE; |
164 | 0 | } |
165 | | |
166 | | /* |
167 | | * helper function that returns the bearer access token as the endpoint authentication method if configured |
168 | | */ |
169 | | static apr_byte_t oidc_proto_endpoint_access_token_bearer(request_rec *r, oidc_cfg_t *cfg, |
170 | 0 | const char *bearer_access_token, char **bearer_auth_str) { |
171 | |
|
172 | 0 | apr_byte_t rv = TRUE; |
173 | |
|
174 | 0 | if (bearer_access_token != NULL) { |
175 | 0 | *bearer_auth_str = apr_psprintf(r->pool, "%s", bearer_access_token); |
176 | 0 | } else { |
177 | 0 | oidc_error(r, "endpoint auth method set to bearer access token but no token is provided"); |
178 | 0 | rv = FALSE; |
179 | 0 | } |
180 | |
|
181 | 0 | return rv; |
182 | 0 | } |
183 | | |
184 | | /* |
185 | | * pick the private signing key (and its accompanying x5t when available) used to sign a private_key_jwt |
186 | | * assertion: client-specific keys take precedence over the module-wide configured private keys |
187 | | */ |
188 | | oidc_jwk_t *oidc_proto_endpoint_auth_pick_signing_key(const oidc_cfg_t *cfg, const apr_array_header_t *client_keys, |
189 | 0 | const char **x5t) { |
190 | 0 | const oidc_jwk_t *jwk_pub = NULL; |
191 | 0 | oidc_jwk_t *jwk = NULL; |
192 | |
|
193 | 0 | *x5t = NULL; |
194 | 0 | if ((client_keys != NULL) && (client_keys->nelts > 0)) { |
195 | 0 | jwk = oidc_util_key_list_first(client_keys, -1, OIDC_JOSE_JWK_SIG_STR); |
196 | 0 | if (jwk && jwk->x5t) |
197 | 0 | *x5t = jwk->x5t; |
198 | 0 | return jwk; |
199 | 0 | } |
200 | 0 | if ((oidc_cfg_private_keys_get(cfg) != NULL) && (oidc_cfg_private_keys_get(cfg)->nelts > 0)) { |
201 | 0 | jwk = oidc_util_key_list_first(oidc_cfg_private_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR); |
202 | 0 | jwk_pub = oidc_util_key_list_first(oidc_cfg_public_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR); |
203 | 0 | if (jwk_pub && jwk_pub->x5t) |
204 | | // populate x5t; at least required for Microsoft Entra ID / Azure AD |
205 | 0 | *x5t = jwk_pub->x5t; |
206 | 0 | } |
207 | 0 | return jwk; |
208 | 0 | } |
209 | | |
210 | | /* |
211 | | * create a JWT assertion signed with the configured private key and add it to the HTTP request as endpoint |
212 | | * authentication |
213 | | */ |
214 | | static apr_byte_t oidc_proto_endpoint_auth_private_key_jwt(request_rec *r, const oidc_cfg_t *cfg, |
215 | | const char *token_endpoint_auth_alg, const char *client_id, |
216 | | const apr_array_header_t *client_keys, |
217 | | oidc_proto_endpoint_auth_aud_t audience, |
218 | 0 | apr_table_t *params) { |
219 | 0 | apr_byte_t rv = FALSE; |
220 | 0 | oidc_jwt_t *jwt = NULL; |
221 | 0 | const oidc_jwk_t *jwk = NULL; |
222 | 0 | const char *x5t = NULL; |
223 | 0 | const char *alg = NULL; |
224 | |
|
225 | 0 | oidc_debug(r, "enter"); |
226 | |
|
227 | 0 | if (oidc_proto_jwt_create(r, client_id, audience, &jwt) == FALSE) |
228 | 0 | return FALSE; |
229 | | |
230 | 0 | jwk = oidc_proto_endpoint_auth_pick_signing_key(cfg, client_keys, &x5t); |
231 | 0 | if (jwk == NULL) { |
232 | 0 | oidc_error(r, "no private signing keys have been configured to use for private_key_jwt client " |
233 | 0 | "authentication (" OIDCPrivateKeyFiles ")"); |
234 | 0 | goto end; |
235 | 0 | } |
236 | | |
237 | 0 | if (x5t != NULL) |
238 | 0 | jwt->header.x5t = apr_pstrdup(r->pool, x5t); |
239 | 0 | jwt->header.kid = apr_pstrdup(r->pool, jwk->kid); |
240 | |
|
241 | 0 | if (token_endpoint_auth_alg != NULL) { |
242 | | // oidc_proto_jwt_sign_and_add will fail later if no corresponding key was configured |
243 | 0 | alg = token_endpoint_auth_alg; |
244 | 0 | } else { |
245 | 0 | alg = oidc_jwk_default_jws_alg(jwk); |
246 | 0 | if (alg == NULL) { |
247 | 0 | oidc_error( |
248 | 0 | r, "no valid signing key (RSA or Elliptic Curve) could be found in " OIDCPrivateKeyFiles); |
249 | 0 | goto end; |
250 | 0 | } |
251 | 0 | } |
252 | 0 | jwt->header.alg = apr_pstrdup(r->pool, alg); |
253 | |
|
254 | 0 | oidc_debug(r, "signing private_key_jwt assertion with algorithm: %s", jwt->header.alg); |
255 | | |
256 | | // this fails if the wrong type of key configured for an explicitly specified algorithm |
257 | 0 | rv = oidc_proto_jwt_sign_and_add(r, params, jwt, jwk); |
258 | |
|
259 | 0 | end: |
260 | |
|
261 | 0 | if (jwt) |
262 | 0 | oidc_jwt_destroy(jwt); |
263 | |
|
264 | 0 | return rv; |
265 | 0 | } |
266 | | |
267 | | /* |
268 | | * Keep this idempotent: retries rebuild the authentication data to obtain a fresh client |
269 | | * assertion, so credentials must replace earlier values rather than create duplicates. |
270 | | */ |
271 | | apr_byte_t oidc_proto_token_endpoint_auth(request_rec *r, oidc_cfg_t *cfg, const char *token_endpoint_auth, |
272 | | const char *token_endpoint_auth_alg, const char *client_id, |
273 | | const char *client_secret, const apr_array_header_t *client_keys, |
274 | | oidc_proto_endpoint_auth_aud_t audience, apr_table_t *params, |
275 | | const char *bearer_access_token, char **basic_auth_str, |
276 | 246 | char **bearer_auth_str) { |
277 | | |
278 | 246 | oidc_debug(r, "token_endpoint_auth=%s", token_endpoint_auth); |
279 | | |
280 | 246 | if (client_id == NULL) { |
281 | 0 | oidc_debug(r, "no client ID set: assume we don't need to authenticate"); |
282 | 0 | return TRUE; |
283 | 0 | } |
284 | | |
285 | | // default is client_secret_basic, but only if a client_secret is set, |
286 | | // otherwise we are a public client |
287 | 246 | if ((token_endpoint_auth == NULL) && (client_secret != NULL)) |
288 | 246 | token_endpoint_auth = OIDC_PROTO_CLIENT_SECRET_BASIC; |
289 | | |
290 | 246 | if ((token_endpoint_auth == NULL) || (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_ENDPOINT_AUTH_NONE) == 0)) { |
291 | 0 | oidc_debug( |
292 | 0 | r, |
293 | 0 | "no client secret is configured or the token endpoint auth method was set to \"%s\"; calling the " |
294 | 0 | "token endpoint without client authentication; only public clients are supported", |
295 | 0 | OIDC_PROTO_ENDPOINT_AUTH_NONE); |
296 | 0 | return oidc_proto_endpoint_auth_none(client_id, params); |
297 | 0 | } |
298 | | |
299 | | // RFC 8705: the client authenticates at the TLS layer with its mutual-TLS client |
300 | | // certificate; only the client_id is passed in the request body |
301 | 246 | if (oidc_cfg_endpoint_auth_is_mtls(token_endpoint_auth)) { |
302 | 0 | oidc_debug(r, |
303 | 0 | "\"%s\": authentication occurs at the TLS layer, adding only the client_id to the request", |
304 | 0 | token_endpoint_auth); |
305 | 0 | return oidc_proto_endpoint_auth_none(client_id, params); |
306 | 0 | } |
307 | | |
308 | | // if no client_secret is set and we don't authenticate using private_key_jwt, |
309 | | // we can only be a public client since the other methods require a client_secret |
310 | 246 | if ((client_secret == NULL) && (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_PRIVATE_KEY_JWT) != 0)) { |
311 | 0 | oidc_debug(r, "no client secret set and not using private_key_jwt, assume we are a public client"); |
312 | 0 | return oidc_proto_endpoint_auth_none(client_id, params); |
313 | 0 | } |
314 | | |
315 | 246 | if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_BASIC) == 0) |
316 | 246 | return oidc_proto_endpoint_client_secret_basic(r, client_id, client_secret, basic_auth_str); |
317 | | |
318 | 0 | if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_POST) == 0) |
319 | 0 | return oidc_proto_endpoint_client_secret_post(r, client_id, client_secret, params); |
320 | | |
321 | 0 | if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_JWT) == 0) |
322 | 0 | return oidc_proto_endpoint_auth_client_secret_jwt(r, client_id, client_secret, audience, params); |
323 | | |
324 | 0 | if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_PRIVATE_KEY_JWT) == 0) |
325 | 0 | return oidc_proto_endpoint_auth_private_key_jwt(r, cfg, token_endpoint_auth_alg, client_id, client_keys, |
326 | 0 | audience, params); |
327 | | |
328 | 0 | if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_BEARER_ACCESS_TOKEN) == 0) { |
329 | 0 | return oidc_proto_endpoint_access_token_bearer(r, cfg, bearer_access_token, bearer_auth_str); |
330 | 0 | } |
331 | | |
332 | 0 | oidc_error(r, "uhm, shouldn't be here..."); |
333 | |
|
334 | 0 | return FALSE; |
335 | 0 | } |