diff options
author | Graham Leggett <minfrin@apache.org> | 2023-04-25 19:52:18 +0200 |
---|---|---|
committer | Graham Leggett <minfrin@apache.org> | 2023-04-25 19:52:18 +0200 |
commit | d8246a15d78d3f51acc19ef8a826c7e34f1f8406 (patch) | |
tree | ffed84ec318046ae84c8018d0d9f742fa2e7028c /modules | |
parent | core: Add the token_checker hook, that allows authentication to take (diff) | |
download | apache2-d8246a15d78d3f51acc19ef8a826c7e34f1f8406.tar.xz apache2-d8246a15d78d3f51acc19ef8a826c7e34f1f8406.zip |
*) mod_autht_jwt: New module to handle RFC 7519 JWT tokens within
bearer tokens, both as part of the aaa framework, and as a way to
generate tokens and pass them to backend servers and services.
*) mod_auth_bearer: New module to handle RFC 6750 Bearer tokens, using
the token_checker hook.
*) mod_autht_core: New module to handle provider aliases for token
authentication.
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1909411 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'modules')
-rw-r--r-- | modules/aaa/config.m4 | 11 | ||||
-rw-r--r-- | modules/aaa/mod_auth_bearer.c | 422 | ||||
-rw-r--r-- | modules/aaa/mod_autht_core.c | 227 | ||||
-rw-r--r-- | modules/aaa/mod_autht_jwt.c | 1089 |
4 files changed, 1749 insertions, 0 deletions
diff --git a/modules/aaa/config.m4 b/modules/aaa/config.m4 index bf3391ffe8..1b59f99f49 100644 --- a/modules/aaa/config.m4 +++ b/modules/aaa/config.m4 @@ -6,6 +6,16 @@ dnl APACHE_MODULE(name, helptext[, objects[, structname[, default[, config]]]]) APACHE_MODPATH_INIT(aaa) +dnl Token modules: modules that parse or reference a token, that may +dnl contain or reference further data like usernames, or IP addresses. +dnl +APACHE_MODULE(autht_jwt, RFC7519 JSON Web Token based authentication control, , , most) + +dnl General Authentication modules; module which implements the +dnl non-autht module specific directives. +dnl +APACHE_MODULE(autht_core, core token authentication module, , , yes) + dnl Authentication modules; modules checking a username and password against a dnl file, database, or other similar magic. dnl @@ -67,6 +77,7 @@ APACHE_MODULE(access_compat, mod_access compatibility, , , yes) dnl these are the front-end authentication modules APACHE_MODULE(auth_basic, basic authentication, , , yes) +APACHE_MODULE(auth_bearer, bearer authentication, , , yes) APACHE_MODULE(auth_form, form authentication, , , most) APACHE_MODULE(auth_digest, RFC2617 Digest authentication, , , most, [ APR_CHECK_APR_DEFINE(APR_HAS_RANDOM) diff --git a/modules/aaa/mod_auth_bearer.c b/modules/aaa/mod_auth_bearer.c new file mode 100644 index 0000000000..7772b21a21 --- /dev/null +++ b/modules/aaa/mod_auth_bearer.c @@ -0,0 +1,422 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This module adds support for https://tools.ietf.org/html/rfc6750 Bearer + * tokens, both as a generator of bearer tokens, and as an acceptor of + * Bearer tokens for authentication. + */ + +#include "apr_strings.h" +#include "apr_hash.h" +#include "apr_lib.h" /* for apr_isspace */ +#define APR_WANT_STRFUNC /* for strcasecmp */ +#include "apr_want.h" + +#include "ap_config.h" +#include "httpd.h" +#include "http_config.h" +#include "http_core.h" +#include "http_log.h" +#include "http_protocol.h" +#include "http_request.h" +#include "ap_provider.h" +#include "ap_expr.h" + +#include "mod_auth.h" + +module AP_MODULE_DECLARE_DATA auth_bearer_module; + +typedef struct { + autht_provider_list *providers; + int authoritative; + ap_expr_info_t *proxy; + int authoritative_set:1; + int proxy_set:1; +} auth_bearer_config_rec; + +static void *create_auth_bearer_dir_config(apr_pool_t *p, char *d) +{ + auth_bearer_config_rec *conf = apr_pcalloc(p, sizeof(*conf)); + + /* Any failures are fatal. */ + conf->authoritative = 1; + + return conf; +} + +static void *merge_auth_bearer_dir_config(apr_pool_t *p, void *basev, void *overridesv) +{ + auth_bearer_config_rec *newconf = apr_pcalloc(p, sizeof(*newconf)); + auth_bearer_config_rec *base = basev; + auth_bearer_config_rec *overrides = overridesv; + + newconf->authoritative = + overrides->authoritative_set ? overrides->authoritative : + base->authoritative; + newconf->authoritative_set = overrides->authoritative_set + || base->authoritative_set; + + newconf->providers = overrides->providers ? overrides->providers : base->providers; + + newconf->proxy = + overrides->proxy_set ? overrides->proxy : base->proxy; + newconf->proxy_set = overrides->proxy_set || base->proxy_set; + + return newconf; +} + +static const char *add_autht_provider(cmd_parms *cmd, void *config, + const char *arg) +{ + auth_bearer_config_rec *conf = (auth_bearer_config_rec*)config; + autht_provider_list *newp; + + newp = apr_pcalloc(cmd->pool, sizeof(autht_provider_list)); + newp->provider_name = arg; + + /* lookup and cache the actual provider now */ + newp->provider = ap_lookup_provider(AUTHT_PROVIDER_GROUP, + newp->provider_name, + AUTHT_PROVIDER_VERSION); + + if (newp->provider == NULL) { + /* by the time they use it, the provider should be loaded and + registered with us. */ + return apr_psprintf(cmd->pool, + "Unknown Autht provider: %s", + newp->provider_name); + } + + if (!newp->provider->check_token) { + /* if it doesn't provide the appropriate function, reject it */ + return apr_psprintf(cmd->pool, + "The '%s' Autht provider doesn't support " + "Bearer Authentication", newp->provider_name); + } + + /* Add it to the list now. */ + if (!conf->providers) { + conf->providers = newp; + } + else { + autht_provider_list *last = conf->providers; + + while (last->next) { + last = last->next; + } + last->next = newp; + } + + return NULL; +} + +static const char *set_authoritative(cmd_parms * cmd, void *config, int flag) +{ + auth_bearer_config_rec *conf = (auth_bearer_config_rec *) config; + + conf->authoritative = flag; + conf->authoritative_set = 1; + + return NULL; +} + +static const char *set_bearer_proxy(cmd_parms * cmd, void *config, + const char *user) +{ + auth_bearer_config_rec *conf = (auth_bearer_config_rec *) config; + const char *err; + + if (!strcasecmp(user, "off")) { + conf->proxy = NULL; + conf->proxy_set = 1; + } + else { + + conf->proxy = + ap_expr_parse_cmd(cmd, user, AP_EXPR_FLAG_STRING_RESULT, + &err, NULL); + if (err) { + return apr_psprintf(cmd->pool, + "Could not parse proxy expression '%s': %s", user, + err); + } + conf->proxy_set = 1; + } + + return NULL; +} + +static const command_rec auth_bearer_cmds[] = +{ + AP_INIT_ITERATE("AuthBearerProvider", add_autht_provider, NULL, OR_AUTHCFG, + "specify the auth providers for a directory or location"), + AP_INIT_FLAG("AuthBearerAuthoritative", set_authoritative, NULL, OR_AUTHCFG, + "Set to 'Off' to allow access control to be passed along to " + "lower modules if the token is not known to this module"), + AP_INIT_TAKE1("AuthBearerProxy", set_bearer_proxy, NULL, OR_AUTHCFG, + "Pass a bearer authentication token over a proxy connection " + "generated using the given expression, 'off' to disable."), + {NULL} +}; + +/* These functions return 0 if client is OK, and proper error status + * if not... either HTTP_UNAUTHORIZED, if we made a check, and it failed, or + * HTTP_INTERNAL_SERVER_ERROR, if things are so totally confused that we + * couldn't figure out how to tell if the client is authorized or not. + * + * If they return DECLINED, and all other modules also decline, that's + * treated by the server core as a configuration error, logged and + * reported as such. + */ + +static void note_bearer_auth_failure(request_rec *r) +{ + apr_table_setn(r->err_headers_out, + (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authenticate" + : "WWW-Authenticate", + apr_pstrcat(r->pool, "Bearer realm=\"", ap_auth_name(r), + "\"", NULL)); +} + +static int hook_note_bearer_auth_failure(request_rec *r, const char *auth_type) +{ + if (strcasecmp(auth_type, "Bearer")) + return DECLINED; + + note_bearer_auth_failure(r); + return OK; +} + +static int get_bearer_auth(request_rec *r, const char **token) +{ + const char *auth_line; + + /* Get the appropriate header */ + auth_line = apr_table_get(r->headers_in, (PROXYREQ_PROXY == r->proxyreq) + ? "Proxy-Authorization" + : "Authorization"); + + if (!auth_line) { + note_bearer_auth_failure(r); + return HTTP_UNAUTHORIZED; + } + + if (strcasecmp(ap_getword(r->pool, &auth_line, ' '), "Bearer")) { + /* Client tried to authenticate using wrong auth scheme */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01614) + "client used wrong authentication scheme: %s", r->uri); + note_bearer_auth_failure(r); + return HTTP_UNAUTHORIZED; + } + + /* Skip leading spaces. */ + while (apr_isspace(*auth_line)) { + auth_line++; + } + + *token = auth_line; + + return OK; +} + +/* Determine the token, and check if we can process the token, for HTTP + * bearer authentication... + */ +static int authenticate_bearer_token(request_rec *r) +{ + auth_bearer_config_rec *conf = ap_get_module_config(r->per_dir_config, + &auth_bearer_module); + const char *sent_token, *current_auth; + int res; + autht_status auth_result; + autht_provider_list *current_provider; + + /* Are we configured to be Bearer auth? */ + current_auth = ap_auth_type(r); + if (!current_auth || strcasecmp(current_auth, "Bearer")) { + return DECLINED; + } + + /* We need an authentication realm. */ + if (!ap_auth_name(r)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01615) + "need AuthName: %s", r->uri); + return HTTP_INTERNAL_SERVER_ERROR; + } + + r->ap_auth_type = (char*)current_auth; + + res = get_bearer_auth(r, &sent_token); + if (res) { + return res; + } + + current_provider = conf->providers; + do { + const autht_provider *provider; + + /* For now, if a provider isn't set, we'll be nice and use the jwt + * provider. + */ + if (!current_provider) { + provider = ap_lookup_provider(AUTHT_PROVIDER_GROUP, + AUTHT_DEFAULT_PROVIDER, + AUTHT_PROVIDER_VERSION); + + if (!provider || !provider->check_token) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01616) + "No Autht provider configured"); + auth_result = AUTHT_GENERAL_ERROR; + break; + } + apr_table_setn(r->notes, AUTHT_PROVIDER_NAME_NOTE, AUTHT_DEFAULT_PROVIDER); + } + else { + provider = current_provider->provider; + apr_table_setn(r->notes, AUTHT_PROVIDER_NAME_NOTE, current_provider->provider_name); + } + + auth_result = provider->check_token(r, "bearer", sent_token); + + apr_table_unset(r->notes, AUTHT_PROVIDER_NAME_NOTE); + + /* Something occurred. Stop checking. */ + if (auth_result != AUTHT_MISMATCH) { + break; + } + + /* If we're not really configured for providers, stop now. */ + if (!conf->providers) { + break; + } + + current_provider = current_provider->next; + } while (current_provider); + + if (auth_result != AUTHT_GRANTED) { + int return_code; + + /* If we're not authoritative, then any error is ignored. */ + if (!(conf->authoritative) && auth_result != AUTHT_DENIED) { + return DECLINED; + } + + switch (auth_result) { + case AUTHT_DENIED: + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() + "bearer token %s: authentication failure for \"%s\": " + "Token Rejected", + sent_token, r->uri); + return_code = HTTP_UNAUTHORIZED; + break; + case AUTHT_EXPIRED: + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() + "bearer token %s: authentication failure for \"%s\": " + "Token has expired", + sent_token, r->uri); + return_code = HTTP_UNAUTHORIZED; + break; + case AUTHT_INVALID: + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() + "bearer token %s: authentication failure for \"%s\": " + "Token is not yet valid", + sent_token, r->uri); + return_code = HTTP_UNAUTHORIZED; + break; + case AUTHT_MISMATCH: + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() + "bearer token %s: did not match '%s': %s", sent_token, + ap_auth_name(r), r->uri); + return_code = HTTP_UNAUTHORIZED; + break; + case AUTH_GENERAL_ERROR: + default: + /* We'll assume that the module has already said what its error + * was in the logs. + */ + return_code = HTTP_INTERNAL_SERVER_ERROR; + break; + } + + /* If we're returning 401, tell them to try again. */ + if (return_code == HTTP_UNAUTHORIZED) { + note_bearer_auth_failure(r); + } + return return_code; + } + + return OK; +} + +/* If we have set claims to be made, create a bearer authentication header + * for the benefit of a proxy or application running behind this server. + */ +static int authenticate_bearer_fixup(request_rec *r) +{ + const char *auth_line, *token, *err; + auth_bearer_config_rec *conf = ap_get_module_config(r->per_dir_config, + &auth_bearer_module); + + if (!conf->proxy) { + return DECLINED; + } + + token = ap_expr_str_exec(r, conf->proxy, &err); + if (err) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02455) + "AuthBearerProxy: could not evaluate token expression for URI '%s': %s", r->uri, err); + return HTTP_INTERNAL_SERVER_ERROR; + } + if (!token || !*token) { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02458) + "AuthBearerProxy: empty token expression for URI '%s', ignoring", r->uri); + + apr_table_unset(r->headers_in, "Authorization"); + + return DECLINED; + } + + auth_line = apr_pstrcat(r->pool, "Bearer ", token, + NULL); + apr_table_setn(r->headers_in, "Authorization", auth_line); + + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02457) + "AuthBearerProxy: \"Authorization: %s\"", + auth_line); + + return OK; +} + +static void register_hooks(apr_pool_t *p) +{ + ap_hook_check_autht(authenticate_bearer_token, NULL, NULL, APR_HOOK_MIDDLE, + AP_AUTH_INTERNAL_PER_CONF); + ap_hook_fixups(authenticate_bearer_fixup, NULL, NULL, APR_HOOK_LAST); + ap_hook_note_auth_failure(hook_note_bearer_auth_failure, NULL, NULL, + APR_HOOK_MIDDLE); +} + +AP_DECLARE_MODULE(auth_bearer) = +{ + STANDARD20_MODULE_STUFF, + create_auth_bearer_dir_config, /* dir config creater */ + merge_auth_bearer_dir_config, /* dir merger --- default is to override */ + NULL, /* server config */ + NULL, /* merge server config */ + auth_bearer_cmds, /* command apr_table_t */ + register_hooks /* register hooks */ +}; diff --git a/modules/aaa/mod_autht_core.c b/modules/aaa/mod_autht_core.c new file mode 100644 index 0000000000..357f5ae241 --- /dev/null +++ b/modules/aaa/mod_autht_core.c @@ -0,0 +1,227 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Security options etc. + * + * Module derived from code originally written by Rob McCool + * + */ + +#include "apr_strings.h" +#include "apr_network_io.h" +#define APR_WANT_STRFUNC +#define APR_WANT_BYTEFUNC +#include "apr_want.h" + +#include "ap_config.h" +#include "httpd.h" +#include "http_config.h" +#include "http_core.h" +#include "http_log.h" +#include "http_request.h" +#include "http_protocol.h" +#include "ap_provider.h" + +#include "mod_auth.h" + +#if APR_HAVE_NETINET_IN_H +#include <netinet/in.h> +#endif + +typedef struct provider_alias_rec { + char *provider_name; + char *provider_alias; + ap_conf_vector_t *sec_auth; + const autht_provider *provider; +} provider_alias_rec; + +typedef struct autht_alias_srv_conf { + apr_hash_t *alias_rec; +} autht_alias_srv_conf; + + +module AP_MODULE_DECLARE_DATA autht_core_module; + +static autht_status authn_alias_check_token(request_rec *r, const char *type, + const char *token) +{ + /* Look up the provider alias in the alias list */ + /* Get the dir_config and call ap_Merge_per_dir_configs() */ + /* Call the real provider->check_password() function */ + /* return the result of the above function call */ + + const char *provider_name = apr_table_get(r->notes, AUTHT_PROVIDER_NAME_NOTE); + autht_status ret = AUTHT_MISMATCH; + autht_alias_srv_conf *authcfg = + (autht_alias_srv_conf *)ap_get_module_config(r->server->module_config, + &autht_core_module); + + if (provider_name) { + provider_alias_rec *prvdraliasrec = apr_hash_get(authcfg->alias_rec, + provider_name, APR_HASH_KEY_STRING); + ap_conf_vector_t *orig_dir_config = r->per_dir_config; + + /* If we found the alias provider in the list, then merge the directory + configurations and call the real provider */ + if (prvdraliasrec) { + r->per_dir_config = ap_merge_per_dir_configs(r->pool, orig_dir_config, + prvdraliasrec->sec_auth); + ret = prvdraliasrec->provider->check_token(r, type, token); + r->per_dir_config = orig_dir_config; + } + } + + return ret; +} + +static void *create_autht_alias_svr_config(apr_pool_t *p, server_rec *s) +{ + + autht_alias_srv_conf *authcfg; + + authcfg = (autht_alias_srv_conf *) apr_pcalloc(p, sizeof(autht_alias_srv_conf)); + authcfg->alias_rec = apr_hash_make(p); + + return (void *) authcfg; +} + +/* Only per-server directive we have is GLOBAL_ONLY */ +static void *merge_autht_alias_svr_config(apr_pool_t *p, void *basev, void *overridesv) +{ + return basev; +} + +static const autht_provider autht_alias_provider = +{ + &authn_alias_check_token +}; + +static const char *authaliassection(cmd_parms *cmd, void *mconfig, const char *arg) +{ + const char *endp = ap_strrchr_c(arg, '>'); + const char *args; + char *provider_alias; + char *provider_name; + int old_overrides = cmd->override; + const char *errmsg; + const autht_provider *provider = NULL; + ap_conf_vector_t *new_auth_config = ap_create_per_dir_config(cmd->pool); + autht_alias_srv_conf *authcfg = + (autht_alias_srv_conf *)ap_get_module_config(cmd->server->module_config, + &autht_core_module); + + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + if (err != NULL) { + return err; + } + + if (endp == NULL) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + "> directive missing closing '>'", NULL); + } + + args = apr_pstrndup(cmd->temp_pool, arg, endp - arg); + + if (!args[0]) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + "> directive requires additional arguments", NULL); + } + + /* Pull the real provider name and the alias name from the block header */ + provider_name = ap_getword_conf(cmd->pool, &args); + provider_alias = ap_getword_conf(cmd->pool, &args); + + if (!provider_name[0] || !provider_alias[0]) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + "> directive requires additional arguments", NULL); + } + + if (strcasecmp(provider_name, provider_alias) == 0) { + return apr_pstrcat(cmd->pool, + "The alias provider name must be different from the base provider name.", NULL); + } + + /* Look up the alias provider to make sure that it hasn't already been registered. */ + provider = ap_lookup_provider(AUTHN_PROVIDER_GROUP, provider_alias, + AUTHN_PROVIDER_VERSION); + if (provider) { + return apr_pstrcat(cmd->pool, "The alias provider ", provider_alias, + " has already be registered previously as either a base provider or an alias provider.", + NULL); + } + + /* walk the subsection configuration to get the per_dir config that we will + merge just before the real provider is called. */ + cmd->override = OR_AUTHCFG | ACCESS_CONF; + errmsg = ap_walk_config(cmd->directive->first_child, cmd, new_auth_config); + cmd->override = old_overrides; + + if (!errmsg) { + provider_alias_rec *prvdraliasrec = apr_pcalloc(cmd->pool, sizeof(provider_alias_rec)); + provider = ap_lookup_provider(AUTHN_PROVIDER_GROUP, provider_name, + AUTHN_PROVIDER_VERSION); + + if (!provider) { + /* by the time they use it, the provider should be loaded and + registered with us. */ + return apr_psprintf(cmd->pool, + "Unknown Authn provider: %s", + provider_name); + } + + /* Save off the new directory config along with the original provider name + and function pointer data */ + prvdraliasrec->sec_auth = new_auth_config; + prvdraliasrec->provider_name = provider_name; + prvdraliasrec->provider_alias = provider_alias; + prvdraliasrec->provider = provider; + apr_hash_set(authcfg->alias_rec, provider_alias, APR_HASH_KEY_STRING, prvdraliasrec); + + /* Register the fake provider so that we get called first */ + ap_register_auth_provider(cmd->pool, AUTHT_PROVIDER_GROUP, + provider_alias, AUTHT_PROVIDER_VERSION, + &autht_alias_provider, + AP_AUTH_INTERNAL_PER_CONF); + } + + return errmsg; +} + +static const command_rec autht_cmds[] = +{ + AP_INIT_RAW_ARGS("<AuthtProviderAlias", authaliassection, NULL, RSRC_CONF, + "container for grouping an authentication provider's " + "directives under a provider alias"), + {NULL} +}; + +static void register_hooks(apr_pool_t *p) +{ + +} + +AP_DECLARE_MODULE(autht_core) = +{ + STANDARD20_MODULE_STUFF, + NULL, /* dir config creater */ + NULL, /* dir merger --- default is to override */ + create_autht_alias_svr_config, /* server config */ + merge_autht_alias_svr_config, /* merge server config */ + autht_cmds, + register_hooks /* register hooks */ +}; + diff --git a/modules/aaa/mod_autht_jwt.c b/modules/aaa/mod_autht_jwt.c new file mode 100644 index 0000000000..ba271a535f --- /dev/null +++ b/modules/aaa/mod_autht_jwt.c @@ -0,0 +1,1089 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This module adds support for https://tools.ietf.org/html/rfc7519 JWT tokens + * as https://tools.ietf.org/html/rfc6750 Bearer tokens, both as a generator + * of JWT bearer tokens, and as an acceptor of JWT Bearer tokens for authentication. + */ + +#include "apr_strings.h" +#include "apr_hash.h" +#include "apr_crypto.h" +#include "apr_jose.h" +#include "apr_lib.h" /* for apr_isspace */ +#include "apr_base64.h" /* for apr_base64_decode et al */ +#define APR_WANT_STRFUNC /* for strcasecmp */ +#include "apr_want.h" + +#include "ap_config.h" +#include "httpd.h" +#include "http_config.h" +#include "http_core.h" +#include "http_log.h" +#include "http_protocol.h" +#include "http_request.h" +#include "util_md5.h" +#include "ap_provider.h" +#include "ap_expr.h" + +#include "mod_auth.h" + +#define CRYPTO_KEY "auth_bearer_context" + +module AP_MODULE_DECLARE_DATA autht_jwt_module; + +typedef enum jws_alg_type_e { + /** No specific type. */ + JWS_ALG_TYPE_NONE = 0, + /** HMAC SHA256 */ + JWS_ALG_TYPE_HS256 = 1, +} jws_alg_type_e; + +typedef struct { + unsigned char *secret; + apr_size_t secret_len; + jws_alg_type_e jws_alg; +} auth_bearer_signature_rec; + +typedef struct { + apr_hash_t *claims; + apr_array_header_t *signs; + apr_array_header_t *verifies; + int signs_set:1; + int verifies_set:1; + int fake_set:1; +} auth_bearer_config_rec; + +typedef struct { + const char *library; + const char *params; + apr_crypto_t **crypto; + int library_set; +} auth_bearer_conf; + +static int auth_bearer_init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, + server_rec *s) { + const apr_crypto_driver_t *driver = NULL; + + /* auth_bearer_init() will be called twice. Don't bother + * going through all of the initialization on the first call + * because it will just be thrown away.*/ + if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG) { + return OK; + } + + while (s) { + + auth_bearer_conf *conf = ap_get_module_config(s->module_config, + &autht_jwt_module); + + if (conf->library_set && !*conf->crypto) { + + const apu_err_t *err = NULL; + apr_status_t rv; + + rv = apr_crypto_init(p); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + APLOGNO() "APR crypto could not be initialised"); + return rv; + } + + rv = apr_crypto_get_driver(&driver, conf->library, conf->params, + &err, p); + if (APR_EREINIT == rv) { + ap_log_error(APLOG_MARK, APLOG_WARNING, rv, s, + APLOGNO() "warning: crypto for '%s' was already initialised, " "using existing configuration", + conf->library); + rv = APR_SUCCESS; + } + if (APR_SUCCESS != rv && err) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + APLOGNO() "The crypto library '%s' could not be loaded: %s (%s: %d)", + conf->library, err->msg, err->reason, err->rc); + return rv; + } + if (APR_ENOTIMPL == rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + APLOGNO() "The crypto library '%s' could not be found", + conf->library); + return rv; + } + if (APR_SUCCESS != rv || !driver) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + APLOGNO() "The crypto library '%s' could not be loaded", + conf->library); + return rv; + } + + rv = apr_crypto_make(conf->crypto, driver, conf->params, p); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + APLOGNO() "The crypto library '%s' could not be initialised", + conf->library); + return rv; + } + + ap_log_error(APLOG_MARK, APLOG_INFO, rv, s, + APLOGNO() "The crypto library '%s' was loaded successfully", + conf->library); + + } + + s = s->next; + } + + return OK; +} + +static void *create_auth_bearer_config(apr_pool_t * p, server_rec *s) +{ + auth_bearer_conf *new = + (auth_bearer_conf *) apr_pcalloc(p, sizeof(auth_bearer_conf)); + + /* if no library has been configured, set the recommended library + * as a sensible default. + */ +#ifdef APU_CRYPTO_RECOMMENDED_DRIVER + new->library = APU_CRYPTO_RECOMMENDED_DRIVER; +#endif + new->crypto = apr_pcalloc(p, sizeof(apr_crypto_t *)); + + return (void *) new; +} + +static void *merge_auth_bearer_config(apr_pool_t * p, void *basev, void *addv) +{ + auth_bearer_conf *new = (auth_bearer_conf *) apr_pcalloc(p, sizeof(auth_bearer_conf)); + auth_bearer_conf *add = (auth_bearer_conf *) addv; + auth_bearer_conf *base = (auth_bearer_conf *) basev; + + new->library = (add->library_set == 0) ? base->library : add->library; + new->params = (add->library_set == 0) ? base->params : add->params; + new->library_set = add->library_set || base->library_set; + + new->crypto = base->crypto; + + return (void *) new; +} + +static void *create_auth_bearer_dir_config(apr_pool_t *p, char *d) +{ + auth_bearer_config_rec *conf = apr_pcalloc(p, sizeof(*conf)); + + conf->claims = apr_hash_make(p); + conf->signs = apr_array_make(p, 1, sizeof(auth_bearer_signature_rec)); + conf->verifies = apr_array_make(p, 1, sizeof(auth_bearer_signature_rec)); + + return conf; +} + +static void *merge_auth_bearer_dir_config(apr_pool_t *p, void *basev, void *overridesv) +{ + auth_bearer_config_rec *newconf = apr_pcalloc(p, sizeof(*newconf)); + auth_bearer_config_rec *base = basev; + auth_bearer_config_rec *overrides = overridesv; + + newconf->claims = apr_hash_overlay(p, overrides->claims, + base->claims); + + newconf->signs = + overrides->signs_set ? overrides->signs : base->signs; + newconf->signs_set = overrides->signs_set || base->signs_set; + + newconf->verifies = + overrides->verifies_set ? overrides->verifies : base->verifies; + newconf->verifies_set = overrides->verifies_set || base->verifies_set; + + return newconf; +} + +static const char *set_jwt_claim(cmd_parms *cmd, void *config, + const char *op, const char *key, const char *expression) +{ + auth_bearer_config_rec *conf = (auth_bearer_config_rec *) config; + const char *err; + + if (!strcasecmp(op, "set")) { + ap_expr_info_t *expr; + + expr = ap_expr_parse_cmd(cmd, expression, AP_EXPR_FLAG_STRING_RESULT, + &err, NULL); + if (err) { + return apr_psprintf(cmd->pool, + "Could not parse claim '%s' expression '%s': %s", key, + expression, err); + } + + apr_hash_set(conf->claims, key, APR_HASH_KEY_STRING, expr); + + } else if (!strcasecmp(op, "unset")) { + + apr_hash_set(conf->claims, key, APR_HASH_KEY_STRING, NULL); + + } else { + + return apr_psprintf(cmd->pool, + "Could not parse claim operation '%s', " + "values should be 'set' or 'unset'", op); + + } + + return NULL; +} + +static const char *set_jwt_sign(cmd_parms * cmd, void *config, + const char *alg, const char *type, const char *sig) +{ + auth_bearer_config_rec *dconf = (auth_bearer_config_rec *) config; + + auth_bearer_signature_rec *srec = apr_array_push(dconf->signs); + + /* handle the algorithm */ + if (!strcasecmp(alg, "none")) { + srec->jws_alg = JWS_ALG_TYPE_NONE; + if (type || sig) { + return "AuthtJwtSign: algorithm 'none' has extra parameters"; + } + } + else if (!strcasecmp(alg, "HS256")) { + srec->jws_alg = JWS_ALG_TYPE_HS256; + } + else { + return apr_psprintf(cmd->pool, "AuthtJwtSign: algorithm not supported: %s", alg); + } + + /* handle the file */ + if (type) { + if (!strcasecmp(type, "file")) { + apr_file_t *file; + apr_finfo_t finfo; + apr_status_t status; + + sig = ap_server_root_relative(cmd->temp_pool, sig); + + status = apr_file_open(&file, sig, APR_READ | APR_BUFFERED, + APR_OS_DEFAULT, cmd->pool); + if (status != APR_SUCCESS) { + char buf[1024]; + apr_strerror(status, buf, sizeof(buf)); + return apr_psprintf(cmd->pool, + "AuthtJwtSign: file '%s' could not be opened: %s", sig, + buf); + } + + status = apr_file_info_get(&finfo, APR_FINFO_TYPE | APR_FINFO_SIZE, + file); + if (status != APR_SUCCESS) { + char buf[1024]; + apr_strerror(status, buf, sizeof(buf)); + return apr_psprintf(cmd->pool, + "AuthtJwtSign: info could not be obtained for '%s': %s", + sig, buf); + } + + srec->secret = apr_palloc(cmd->pool, finfo.size); + srec->secret_len = finfo.size; + + status = apr_file_read_full(file, srec->secret, + srec->secret_len, NULL); + if (status != APR_SUCCESS) { + char buf[1024]; + apr_strerror(status, buf, sizeof(buf)); + return apr_psprintf(cmd->pool, + "AuthtJwtSign: file '%s' could not be read: %s", sig, + buf); + } + + apr_file_close(file); + + } + else { + return apr_psprintf(cmd->pool, + "AuthtJwtSign: parameter '%s' is not 'file'", type); + } + } + + dconf->signs_set = 1; + + return NULL; +} + +static const char *set_jwt_verify(cmd_parms * cmd, void *config, + const char *alg, const char *type, const char *sig) +{ + auth_bearer_config_rec *dconf = (auth_bearer_config_rec *) config; + + auth_bearer_signature_rec *srec = apr_array_push(dconf->verifies); + + /* handle the algorithm */ + if (!strcasecmp(alg, "none")) { + srec->jws_alg = JWS_ALG_TYPE_NONE; + if (type || sig) { + return "AuthtJwtVerify: algorithm 'none' has extra parameters"; + } + } + else if (!strcasecmp(alg, "HS256")) { + srec->jws_alg = JWS_ALG_TYPE_HS256; + } + else { + return apr_psprintf(cmd->pool, "AuthtJwtVerify: algorithm not supported: %s", alg); + } + + /* handle the file */ + if (type) { + if (!strcasecmp(type, "file")) { + apr_file_t *file; + apr_finfo_t finfo; + apr_status_t status; + + sig = ap_server_root_relative(cmd->temp_pool, sig); + + status = apr_file_open(&file, sig, APR_READ | APR_BUFFERED, + APR_OS_DEFAULT, cmd->pool); + if (status != APR_SUCCESS) { + char buf[1024]; + apr_strerror(status, buf, sizeof(buf)); + return apr_psprintf(cmd->pool, + "AuthtJwtVerify: file '%s' could not be opened: %s", sig, + buf); + } + + status = apr_file_info_get(&finfo, APR_FINFO_TYPE | APR_FINFO_SIZE, + file); + if (status != APR_SUCCESS) { + char buf[1024]; + apr_strerror(status, buf, sizeof(buf)); + return apr_psprintf(cmd->pool, + "AuthtJwtVerify: info could not be obtained for '%s': %s", + sig, buf); + } + + srec->secret = apr_palloc(cmd->pool, finfo.size); + srec->secret_len = finfo.size; + + status = apr_file_read_full(file, srec->secret, + srec->secret_len, NULL); + if (status != APR_SUCCESS) { + char buf[1024]; + apr_strerror(status, buf, sizeof(buf)); + return apr_psprintf(cmd->pool, + "AuthtJwtVerify: file '%s' could not be read: %s", sig, + buf); + } + + apr_file_close(file); + + } + else { + return apr_psprintf(cmd->pool, + "AuthtJwtVerify: parameter '%s' is not 'file'", type); + } + } + + dconf->verifies_set = 1; + + return NULL; +} + +static const char *set_jwt_driver(cmd_parms * cmd, void *config, const char *arg) +{ + auth_bearer_conf *conf = + (auth_bearer_conf *)ap_get_module_config(cmd->server->module_config, + &autht_jwt_module); + + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + + if (err != NULL) { + return err; + } + + conf->library = ap_getword_conf(cmd->pool, &arg); + conf->params = arg; + conf->library_set = 1; + + return NULL; +} + +static const command_rec auth_bearer_cmds[] = +{ + AP_INIT_TAKE13("AuthtJwtVerify", set_jwt_verify, NULL, RSRC_CONF|OR_AUTHCFG, + "The JWS signing algorithm and passphrase/key to verify an incoming JWT token"), + AP_INIT_TAKE13("AuthtJwtSign", set_jwt_sign, NULL, RSRC_CONF|OR_AUTHCFG, + "The JWS signing algorithm and passphrase/key to sign an outgoing JWT token"), + + AP_INIT_TAKE23("AuthtJwtClaim", set_jwt_claim, NULL, OR_AUTHCFG, + "Set a claim with the given name and expression, or " + "unset the claim with the given name."), + + AP_INIT_RAW_ARGS("AuthtJwtDriver", set_jwt_driver, NULL, RSRC_CONF, + "The underlying crypto library driver to use"), + + {NULL} +}; + +typedef struct claim_iter_t { + request_rec *r; + apr_json_value_t *object; +} claim_iter_t; + +static int claim_iter(void *ctx, const void *key, apr_ssize_t klen, + const void *val) +{ + const char *err, *value; + claim_iter_t *iter = ctx; + request_rec *r = iter->r; + + value = ap_expr_str_exec(r, val, &err); + if (err) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() + "AuthtJwtClaim: could not evaluate '%s' expression " + "'%s' for URI '%s': %s", + (char * )key, (char * )val, r->uri, err); + return FALSE; + } + + + apr_json_object_set(iter->object, key, klen, + apr_json_string_create(r->pool, value, strlen(value)), r->pool); + + return TRUE; +} + +static apr_status_t sign_cb(apr_bucket_brigade *bb, apr_jose_t *jose, + apr_jose_signature_t *signature, void *ctx, apr_pool_t *pool) { + auth_bearer_signature_rec *srec = NULL; + request_rec *r = ctx; + + auth_bearer_conf *sconf = ap_get_module_config(r->server->module_config, + &autht_jwt_module); + + auth_bearer_config_rec *conf = ap_get_module_config(r->per_dir_config, + &autht_jwt_module); + + if (conf->signs_set) { + srec = (auth_bearer_signature_rec *) conf->signs->elts; + } + + if (srec) { + switch (srec->jws_alg) { + case JWS_ALG_TYPE_NONE: { + + return APR_SUCCESS; + } + case JWS_ALG_TYPE_HS256: { + apr_bucket *e; + apr_crypto_key_rec_t *krec; + apr_crypto_key_t *key = NULL; + apr_crypto_digest_t *digest = NULL; + apr_crypto_digest_rec_t *rec; + char * buf; + apr_status_t status; + + if (!*sconf->crypto) { + jose->result.msg = "token could not be signed"; + jose->result.reason = "no crypto driver configured (set AuthtJwtDriver)"; + return APR_EGENERAL; + } + + krec = apr_crypto_key_rec_make(APR_CRYPTO_KTYPE_HMAC, pool); + + krec->k.hmac.digest = APR_CRYPTO_DIGEST_SHA256; + krec->k.hmac.secret = srec->secret; + krec->k.hmac.secretLen = srec->secret_len; + + status = apr_crypto_key(&key, krec, *sconf->crypto, pool); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be signed"; + return status; + } + + rec = apr_crypto_digest_rec_make(APR_CRYPTO_DTYPE_SIGN, pool); + + status = apr_crypto_digest_init(&digest, key, rec, pool); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be signed"; + return status; + } + + for (e = APR_BRIGADE_FIRST(bb); e != APR_BRIGADE_SENTINEL(bb); e = + APR_BUCKET_NEXT(e)) { + const char *str; + apr_size_t len; + + /* If we see an EOS, don't bother doing anything more. */ + if (APR_BUCKET_IS_EOS(e)) { + break; + } + + status = apr_bucket_read(e, &str, &len, APR_BLOCK_READ); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be signed"; + return status; + } + + status = apr_crypto_digest_update(digest, + (const unsigned char *) str, len); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be signed"; + return status; + } + } + + status = apr_crypto_digest_final(digest); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be signed"; + return status; + } + + signature->sig.data = rec->d.sign.s; + signature->sig.len = rec->d.sign.slen; + + return APR_SUCCESS; + } + } + } + else { + /* algorithm is none */ + return APR_SUCCESS; + } + + return APR_ENOTIMPL; +} + +/* If we have set claims to be made, create a JWT token. + */ +static const char *jwt_get_token(request_rec *r) +{ + claim_iter_t iter = { 0 }; + apr_json_value_t *claims; + apr_json_value_t *protect; + apr_jose_t jwt = { 0 }; + apr_jose_t jws = { 0 }; + apr_jose_signature_t signature = { 0 }; + auth_bearer_signature_rec *srec = NULL; + apr_bucket_brigade *bb; + char *auth_line; + apr_size_t len; + apr_off_t offset; + apr_status_t status; + + auth_bearer_config_rec *conf = ap_get_module_config(r->per_dir_config, + &autht_jwt_module); + + apr_jose_cb_t cb = { 0 }; + + cb.sign = sign_cb; + cb.ctx = r; + + if (!conf->claims || !apr_hash_count(conf->claims)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_SUCCESS, r, + APLOGNO() "AuthtJwtClaim: could not encode a JWT token for URI '%s': no claims", + r->uri); + return "error:no-claims"; + } + + if (conf->verifies_set) { + srec = (auth_bearer_signature_rec *)conf->verifies->elts; + } + + /* create a JWT containing the claims */ + claims = apr_json_object_create(r->pool); + iter.object = claims; + iter.r = r; + + /* iterate through our claims */ + if (!apr_hash_do(claim_iter, &iter, conf->claims)) { + return "error:claim-failed"; + } + + apr_jose_jwt_make(&jwt, claims, r->pool); + protect = apr_json_object_create(r->pool); + + apr_json_object_set(protect, APR_JOSE_JWSE_TYPE, + APR_JSON_VALUE_STRING, + apr_json_string_create(r->pool, APR_JOSE_JWSE_TYPE_JWT, + APR_JSON_VALUE_STRING), r->pool); + + if (srec) { + /* which signature type do we have? */ + switch (srec->jws_alg) { + case JWS_ALG_TYPE_NONE: { + apr_json_object_set(protect, APR_JOSE_JWKSE_ALGORITHM, + APR_JSON_VALUE_STRING, + apr_json_string_create(r->pool, APR_JOSE_JWA_NONE, + APR_JSON_VALUE_STRING), r->pool); + + break; + } + case JWS_ALG_TYPE_HS256: { + apr_json_object_set(protect, APR_JOSE_JWKSE_ALGORITHM, + APR_JSON_VALUE_STRING, + apr_json_string_create(r->pool, APR_JOSE_JWA_HS256, + APR_JSON_VALUE_STRING), r->pool); + + break; + } + } + + } + else { + /* no srec defaults to none */ + apr_json_object_set(protect, APR_JOSE_JWKSE_ALGORITHM, + APR_JSON_VALUE_STRING, + apr_json_string_create(r->pool, APR_JOSE_JWA_NONE, + APR_JSON_VALUE_STRING), r->pool); + } + + apr_jose_signature_make(&signature, NULL, protect, r->pool); + apr_jose_jws_make(&jws, &signature, NULL, &jwt, r->pool); + + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + + status = apr_jose_encode(bb, NULL, NULL, &jws, &cb, r->pool); + if (APR_SUCCESS != status) { + const apu_err_t *err = apr_jose_error(&jws); + ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, + APLOGNO() "AuthtJwtClaim: could not encode a JWT token for URI '%s': %s: %s", + r->uri, err->msg, err->reason); + return "error:could-not-encode"; + } + + apr_brigade_length(bb, 1, &offset); + auth_line = apr_pcalloc(r->pool, offset + 1); + len = offset; + apr_brigade_flatten(bb, auth_line, &len); + auth_line[offset] = 0; + + return auth_line; +} + +static const char *jwt_expr_var_fn(ap_expr_eval_ctx_t *ctx, const void *data) +{ + char *var = (char *)data; + + if (var && *var && ctx->r && ap_cstr_casecmp(var, "TOKEN") == 0) { + return jwt_get_token(ctx->r); + } + return NULL; +} + +static int jwt_expr_lookup(ap_expr_lookup_parms *parms) +{ + switch (parms->type) { + case AP_EXPR_FUNC_VAR: + /* for now, we just handle everything that starts with JWT_. + */ + if (strncasecmp(parms->name, "JWT_", 4) == 0) { + *parms->func = jwt_expr_var_fn; + *parms->data = parms->name + 4; + return OK; + } + break; + } + return DECLINED; +} + +static apr_status_t verify_cb(apr_bucket_brigade *bb, + apr_jose_t *jose, apr_jose_signature_t *signature, void *ctx, + int *vflags, apr_pool_t *pool) +{ + request_rec *r = ctx; + apr_json_kv_t *alg = NULL; + + auth_bearer_conf *sconf = ap_get_module_config(r->server->module_config, + &autht_jwt_module); + + auth_bearer_config_rec *conf = ap_get_module_config(r->per_dir_config, + &autht_jwt_module); + + int alg_supported = 0; + + if (signature) { + apr_json_value_t *ph = signature->protected_header; + + if (ph && ph->type == APR_JSON_OBJECT) { + + alg = apr_json_object_get(ph, APR_JOSE_JWKSE_ALGORITHM, + APR_JSON_VALUE_STRING); + + } + } + + if (!alg) { + apr_errprintf(&jose->result, r->pool, "", APR_EGENERAL, + "JWT token protected header has no '" + APR_JOSE_JWKSE_ALGORITHM + "' for URI '%s'", + r->uri); + return APR_EGENERAL; + } + + if (alg->v->type != APR_JSON_STRING) { + apr_errprintf(&jose->result, r->pool, "", APR_EGENERAL, + "JWT token protected header '" + APR_JOSE_JWKSE_ALGORITHM + "' is not a string for URI '%s'", + r->uri); + return APR_EGENERAL; + } + + /* first pass, is our algorithm supported? */ + for (int i = 0; i < conf->verifies->nelts; i++) { + auth_bearer_signature_rec *srec = &APR_ARRAY_IDX(conf->verifies, + i, auth_bearer_signature_rec); + + /* which signature type do we have? */ + switch (srec->jws_alg) { + case JWS_ALG_TYPE_NONE: { + if (!strncmp(alg->v->value.string.p, "none", + alg->v->value.string.len)) { + alg_supported = 1; + } + break; + } + case JWS_ALG_TYPE_HS256: { + if (!strncmp(alg->v->value.string.p, "HS256", + alg->v->value.string.len)) { + alg_supported = 1; + } + break; + } + } + + } + + /* we don't support the algorithm */ + if (!alg_supported) { + apr_errprintf(&jose->result, r->pool, "", APR_ENODIGEST, + "JWT token protected header '" + APR_JOSE_JWKSE_ALGORITHM + "' %s is not supported for URI '%s'", + alg->v->value.string.p, r->uri); + return APR_ENODIGEST; + } + + /* second pass, does the signature match? */ + for (int i = 0; i < conf->verifies->nelts; i++) { + auth_bearer_signature_rec *srec = &APR_ARRAY_IDX(conf->verifies, + i, auth_bearer_signature_rec); + + /* which signature type do we have? */ + switch (srec->jws_alg) { + case JWS_ALG_TYPE_NONE: { + if (!strncmp(alg->v->value.string.p, "none", + alg->v->value.string.len)) { + return APR_SUCCESS; + } + break; + } + case JWS_ALG_TYPE_HS256: { + if (!strncmp(alg->v->value.string.p, "HS256", + alg->v->value.string.len)) { + + apr_bucket *e; + apr_crypto_key_rec_t *krec; + apr_crypto_key_t *key = NULL; + apr_crypto_digest_t *digest = NULL; + apr_crypto_digest_rec_t *rec; + char * buf; + apr_status_t status; + + if (!*sconf->crypto) { + jose->result.msg = "token could not be verified"; + jose->result.reason = "no crypto driver configured (set AuthtJwtDriver)"; + return APR_EGENERAL; + } + + krec = apr_crypto_key_rec_make(APR_CRYPTO_KTYPE_HMAC, pool); + + krec->k.hmac.digest = APR_CRYPTO_DIGEST_SHA256; + krec->k.hmac.secret = srec->secret; + krec->k.hmac.secretLen = srec->secret_len; + + status = apr_crypto_key(&key, krec, *sconf->crypto, pool); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be verified"; + return status; + } + + rec = apr_crypto_digest_rec_make(APR_CRYPTO_DTYPE_SIGN, pool); + + status = apr_crypto_digest_init(&digest, key, rec, pool); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be verified"; + return status; + } + + for (e = APR_BRIGADE_FIRST(bb); e != APR_BRIGADE_SENTINEL(bb); e = + APR_BUCKET_NEXT(e)) { + const char *str; + apr_size_t len; + + /* If we see an EOS, don't bother doing anything more. */ + if (APR_BUCKET_IS_EOS(e)) { + break; + } + + status = apr_bucket_read(e, &str, &len, APR_BLOCK_READ); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be verified"; + return status; + } + + status = apr_crypto_digest_update(digest, + (const unsigned char *) str, len); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be verified"; + return status; + } + } + + status = apr_crypto_digest_final(digest); + if (status != APR_SUCCESS) { + jose->result.reason = buf = apr_pcalloc(pool, HUGE_STRING_LEN); + apr_strerror(status, buf, HUGE_STRING_LEN); + jose->result.msg = "token could not be verified"; + return status; + } + + if (signature->sig.len == rec->d.sign.slen && + !memcmp(signature->sig.data, rec->d.sign.s, rec->d.sign.slen)) { + return APR_SUCCESS; + } + + } + break; + } + } + + } + + /* no match, oh well */ + apr_errprintf(&jose->result, r->pool, "", APR_ENODIGEST, + "JWT token protected header '" + APR_JOSE_JWKSE_ALGORITHM + "' %s is not supported for URI '%s'", + alg->v->value.string.p, r->uri); + return APR_ENOVERIFY; +} + +static autht_status check_token(request_rec *r, const char *type, + const char *token) +{ + apr_bucket_brigade *bb; + apr_jose_t *jose = NULL; + apr_json_kv_t *kv; + apr_status_t status; + + apr_jose_cb_t cb; + + apr_table_t *e = r->subprocess_env; + + const char *aud = NULL; + const char *sub = NULL; + + apr_int64_t exp; + apr_int64_t nbf; + + int exp_set = 0; + int nbf_set = 0; + + cb.verify = verify_cb; + cb.decrypt = NULL; + cb.ctx = r; + + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + + if (token) { + apr_brigade_write(bb, NULL, NULL, token, strlen(token)); + } + + status = apr_jose_decode(&jose, "JWT", bb, &cb, 10, APR_JOSE_FLAG_NONE, r->pool); + + if (APR_SUCCESS != status) { + const apu_err_t *err = apr_jose_error(jose); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + APLOGNO() "AuthtJwt: could not decode a JWT token for URI '%s': %s: %s", + r->uri, err->msg, err->reason); + return AUTHT_DENIED; + } + + if (jose->type != APR_JOSE_TYPE_JWT) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + APLOGNO() "AuthtJwt: JOSE token was not a JWT token for URI '%s'", + r->uri); + return AUTHT_DENIED; + } + + /* first pass - identity sub and aud */ + kv = apr_json_object_first(jose->jose.jwt->claims); + do { + + /* ignore any key that isn't a string */ + if (kv->k->type != APR_JSON_STRING) { + continue; + } + + if (!strncmp("aud", kv->k->value.string.p, kv->k->value.string.len)) { + if (kv->v->type == APR_JSON_STRING) { + aud = apr_pstrndup(r->pool, kv->v->value.string.p, + kv->v->value.string.len); + } + } + + if (!strncmp("sub", kv->k->value.string.p, kv->k->value.string.len)) { + if (kv->v->type == APR_JSON_STRING) { + sub = apr_pstrndup(r->pool, kv->v->value.string.p, + kv->v->value.string.len); + } + } + + if (!strncmp("exp", kv->k->value.string.p, kv->k->value.string.len)) { + if (kv->v->type == APR_JSON_LONG) { + exp = kv->v->value.lnumber; + exp_set = 1; + } + } + + if (!strncmp("nbf", kv->k->value.string.p, kv->k->value.string.len)) { + if (kv->v->type == APR_JSON_LONG) { + nbf = kv->v->value.lnumber; + nbf_set = 1; + } + } + + + } while ((kv = apr_json_object_next(jose->jose.jwt->claims, kv))); + + if (!aud) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + APLOGNO() "AuthtJwt: JWT token 'aud' value was missing and did not match AuthName '%s' for URI '%s'", + ap_auth_name(r), r->uri); + return AUTHT_MISMATCH; + } + + if (strcmp(aud, ap_auth_name(r))) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + APLOGNO() "AuthtJwt: JWT token 'aud' value '%s' did not match AuthName '%s' for URI '%s'", + aud, ap_auth_name(r), r->uri); + return AUTHT_MISMATCH; + } + + if (exp_set || nbf_set) { + apr_time_t now = apr_time_now(); + + if (exp_set && + exp < apr_time_sec(now)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + APLOGNO() "AuthtJwt: JWT token is expired (%" + APR_INT64_T_FMT " < %" APR_TIME_T_FMT ") for URI '%s'", + exp, apr_time_sec(now), r->uri); + return AUTHT_EXPIRED; + } + + if (nbf_set && + nbf > apr_time_sec(now)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + APLOGNO() "AuthtJwt: JWT token is not yet valid (%" + APR_INT64_T_FMT " > %" APR_TIME_T_FMT ") for URI '%s'", + nbf, apr_time_sec(now), r->uri); + return AUTHT_INVALID; + } + } + + /* we are good at this point - accept the token */ + + if (sub) { + r->user = apr_pstrdup(r->pool, sub); + } + + /* second pass - add all string claims to the environment, prefixed by TOKEN_ */ + kv = apr_json_object_first(jose->jose.jwt->claims); + do { + char *key, *val; + int j; + + /* ignore anything that isn't a string */ + if (kv->k->type != APR_JSON_STRING || kv->v->type != APR_JSON_STRING) { + continue; + } + + key = apr_psprintf(r->pool, AUTHT_PREFIX "%.*s", (int)kv->k->value.string.len, kv->k->value.string.p); + j = sizeof(AUTHT_PREFIX); + while (key[j]) { + if (apr_isalnum(key[j])) { + key[j] = apr_toupper(key[j]); + } + else { + key[j] = '_'; + } + j++; + } + + val = apr_pstrndup(r->pool, kv->v->value.string.p, + kv->v->value.string.len); + + apr_table_setn(e, key, val); + + } while ((kv = apr_json_object_next(jose->jose.jwt->claims, kv))); + + return AUTHT_GRANTED; +} + +static const autht_provider autht_jwt_provider = +{ + &check_token +}; + +static void register_hooks(apr_pool_t *p) +{ + ap_register_auth_provider(p, AUTHT_PROVIDER_GROUP, "jwt", + AUTHT_PROVIDER_VERSION, + &autht_jwt_provider, AP_AUTH_INTERNAL_PER_CONF); + ap_hook_expr_lookup(jwt_expr_lookup, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_post_config(auth_bearer_init, NULL, NULL, APR_HOOK_LAST); +} + +AP_DECLARE_MODULE(autht_jwt) = +{ + STANDARD20_MODULE_STUFF, + create_auth_bearer_dir_config, /* dir config creater */ + merge_auth_bearer_dir_config, /* dir merger --- default is to override */ + create_auth_bearer_config, /* server config */ + merge_auth_bearer_config, /* merge server config */ + auth_bearer_cmds, /* command apr_table_t */ + register_hooks /* register hooks */ +}; |