2 * Copyright (c) 2019 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
17 * 3. Neither the name of the Institute nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
21 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
22 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35 * This is a plugin by which bx509d can validate JWT Bearer tokens using the
43 * cjwt_jqk = PATH-TO-JWK-PEM-FILE
47 * where AUDIENCE-FOR-KDC is the value of the "audience" (i.e., the target) of
58 #include <common_plugin.h>
61 #include <token_validator_plugin.h>
62 #include <cjwt/cjwt.h>
68 get_kv(krb5_context context
, const char *realm
, const char *k
, const char *k2
)
70 return krb5_config_get_string(context
, NULL
, "bx509", "realms", realm
,
74 static krb5_error_code
75 get_issuer_pubkeys(krb5_context context
,
81 krb5_error_code save_ret
= 0;
86 previous
->data
= current
->data
= next
->data
= 0;
87 previous
->length
= current
->length
= next
->length
= 0;
89 if ((v
= get_kv(context
, realm
, "cjwt_jwk_next", NULL
)) &&
91 (ret
= rk_undumpdata(v
, &next
->data
, &next
->length
)))
93 if ((v
= get_kv(context
, realm
, "cjwt_jwk_previous", NULL
)) &&
95 (ret
= rk_undumpdata(v
, &previous
->data
, &previous
->length
)) &&
98 if ((v
= get_kv(context
, realm
, "cjwt_jwk_current", NULL
)) &&
100 (ret
= rk_undumpdata(v
, ¤t
->data
, ¤t
->length
)) &&
104 krb5_set_error_message(context
, EINVAL
, "jwk issuer key not specified in "
105 "[bx509]->realm->%s->cjwt_jwk_{previous,current,next}",
107 if (!previous
->length
&& !current
->length
&& !next
->length
)
108 krb5_set_error_message(context
, save_ret
,
109 "Could not read jwk issuer public key files");
110 if (current
->length
&& current
->length
== next
->length
&&
111 memcmp(current
->data
, next
->data
, next
->length
) == 0) {
116 if (current
->length
&& current
->length
== previous
->length
&&
117 memcmp(current
->data
, previous
->data
, previous
->length
) == 0) {
118 free(previous
->data
);
120 previous
->length
= 0;
123 if (previous
->data
== NULL
&& current
->data
== NULL
&& next
->data
== NULL
)
124 return krb5_set_error_message(context
, ENOENT
, "No JWKs found"),
129 static krb5_error_code
130 check_audience(krb5_context context
,
133 const char * const *audiences
,
139 krb5_set_error_message(context
, EACCES
, "JWT bearer token has no "
143 for (i
= 0; i
< jwt
->aud
->count
; i
++)
144 for (k
= 0; k
< naudiences
; k
++)
145 if (strcasecmp(audiences
[k
], jwt
->aud
->names
[i
]) == 0)
147 krb5_set_error_message(context
, EACCES
, "JWT bearer token's audience "
148 "does not match any expected audience");
152 static krb5_error_code
153 get_princ(krb5_context context
,
156 krb5_principal
*actual_principal
)
159 const char *force_realm
= NULL
;
163 if (jwt
->private_claims
) {
166 if ((jval
= cJSON_GetObjectItem(jwt
->private_claims
, "authz_sub")))
167 return krb5_parse_name(context
, jval
->valuestring
, actual_principal
);
171 if (jwt
->sub
== NULL
) {
172 krb5_set_error_message(context
, EACCES
, "JWT token lacks 'sub' "
176 if ((domain
= strchr(jwt
->sub
, '@'))) {
177 force_realm
= get_kv(context
, realm
, "cjwt_force_realm", ++domain
);
178 ret
= krb5_parse_name(context
, jwt
->sub
, actual_principal
);
180 ret
= krb5_parse_name_flags(context
, jwt
->sub
,
181 KRB5_PRINCIPAL_PARSE_NO_REALM
,
185 krb5_set_error_message(context
, ret
, "JWT token 'sub' not a valid "
186 "principal name: %s", jwt
->sub
);
187 else if (force_realm
)
188 ret
= krb5_principal_set_realm(context
, *actual_principal
, realm
);
189 else if (domain
== NULL
)
190 ret
= krb5_principal_set_realm(context
, *actual_principal
, realm
);
191 /* else leave the domain as the realm */
195 static KRB5_LIB_CALL krb5_error_code
197 krb5_context context
,
199 const char *token_type
,
201 const char * const *audiences
,
203 krb5_boolean
*result
,
204 krb5_principal
*actual_principal
,
205 krb5_times
*token_times
)
207 heim_octet_string jwk_previous
;
208 heim_octet_string jwk_current
;
209 heim_octet_string jwk_next
;
212 char *defrealm
= NULL
;
215 if (strcmp(token_type
, "Bearer") != 0)
216 return KRB5_PLUGIN_NO_HANDLE
; /* Not us */
218 if ((tokstr
= calloc(1, token
->length
+ 1)) == NULL
)
220 memcpy(tokstr
, token
->data
, token
->length
);
223 ret
= krb5_get_default_realm(context
, &defrealm
);
225 krb5_set_error_message(context
, ret
, "could not determine default "
233 ret
= get_issuer_pubkeys(context
, realm
, &jwk_previous
, &jwk_current
,
241 if (jwk_current
.length
&& jwk_current
.data
)
242 ret
= cjwt_decode(tokstr
, 0, &jwt
, jwk_current
.data
,
244 if (ret
&& jwk_next
.length
&& jwk_next
.data
)
245 ret
= cjwt_decode(tokstr
, 0, &jwt
, jwk_next
.data
,
247 if (ret
&& jwk_previous
.length
&& jwk_previous
.data
)
248 ret
= cjwt_decode(tokstr
, 0, &jwt
, jwk_previous
.data
,
249 jwk_previous
.length
);
250 free(jwk_previous
.data
);
251 free(jwk_current
.data
);
253 jwk_previous
.data
= jwk_current
.data
= jwk_next
.data
= NULL
;
259 krb5_set_error_message(context
, EINVAL
, "JWT validation failed");
263 if (jwt
->header
.alg
== alg_none
) {
264 krb5_set_error_message(context
, EINVAL
, "JWT signature algorithm "
271 krb5_set_error_message(context
, EINVAL
, "invalid JWT format");
275 krb5_set_error_message(context
, EINVAL
, "JWT signature validation "
276 "failed (wrong issuer?)");
280 krb5_set_error_message(context
, ret
, "misc token validation error");
285 /* Success; check audience */
286 if ((ret
= check_audience(context
, realm
, jwt
, audiences
, naudiences
))) {
292 /* Success; extract principal name */
293 if ((ret
= get_princ(context
, realm
, jwt
, actual_principal
)) == 0) {
294 token_times
->authtime
= jwt
->iat
.tv_sec
;
295 token_times
->starttime
= jwt
->nbf
.tv_sec
;
296 token_times
->endtime
= jwt
->exp
.tv_sec
;
297 token_times
->renew_till
= jwt
->exp
.tv_sec
;
306 static KRB5_LIB_CALL krb5_error_code
307 hcjwt_init(krb5_context context
, void **c
)
313 static KRB5_LIB_CALL
void
318 static krb5plugin_token_validator_ftable plug_desc
=
319 { 1, hcjwt_init
, hcjwt_fini
, validate
};
321 static krb5plugin_token_validator_ftable
*plugs
[] = { &plug_desc
};
324 hcjwt_get_instance(const char *libname
)
326 if (strcmp(libname
, "krb5") == 0)
327 return krb5_get_instance(libname
);
331 krb5_plugin_load_ft kdc_token_validator_plugin_load
;
333 krb5_error_code KRB5_CALLCONV
334 kdc_token_validator_plugin_load(heim_pcontext context
,
335 krb5_get_instance_func_t
*get_instance
,
337 krb5_plugin_common_ftable_cp
**plugins
)
339 *get_instance
= hcjwt_get_instance
;
340 *num_plugins
= sizeof(plugs
) / sizeof(plugs
[0]);
341 *plugins
= (krb5_plugin_common_ftable_cp
*)plugs
;