diff options
Diffstat (limited to 'modules/aaa/mod_autht_jwt.c')
-rw-r--r-- | modules/aaa/mod_autht_jwt.c | 1089 |
1 files changed, 1089 insertions, 0 deletions
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 */ +}; |