| /* 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. | |
| */ | |
| /* apr_jose support requires >= 1.7 */ | |
| #if APU_MAJOR_VERSION > 1 || \ | |
| (APU_MAJOR_VERSION == 1 && APU_MINOR_VERSION > 6) | |
| #define HAVE_APU_JOSE 1 | |
| #endif | |
| #include "httpd.h" | |
| #include "http_config.h" | |
| #ifdef HAVE_APU_JOSE | |
| #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 "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; | |
| unsigned int signs_set :1, | |
| verifies_set :1; | |
| } auth_bearer_config_rec; | |
| typedef struct { | |
| const char *library; | |
| const char *params; | |
| apr_crypto_t **crypto; | |
| unsigned int library_set :1; | |
| } 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(10432) "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(10433) "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(10434) "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(10435) "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(10436) "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(10437) "The crypto library '%s' could not be initialised", | |
| conf->library); | |
| return rv; | |
| } | |
| ap_log_error(APLOG_MARK, APLOG_INFO, rv, s, | |
| APLOGNO(10438) "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(10439) | |
| "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(10440) "AuthtJwtClaim: could not encode a JWT token for URI '%s': no claims", | |
| r->uri); | |
| return "error:no-claims"; | |
| } | |
| /* sign with the first key, if present */ | |
| if (conf->signs_set) { | |
| srec = (auth_bearer_signature_rec *)conf->signs->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(10441) "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(10442) "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(10443) "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(10444) "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(10445) "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(10446) "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(10447) "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) - 1; /* string length of "TOKEN_", excluding the trailing NUL */ | |
| 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 */ | |
| }; | |
| #else | |
| static const command_rec auth_bearer_cmds[] = | |
| { | |
| {NULL} | |
| }; | |
| static void register_hooks(apr_pool_t *p) | |
| { | |
| } | |
| AP_DECLARE_MODULE(autht_jwt) = | |
| { | |
| STANDARD20_MODULE_STUFF, | |
| NULL, /* dir config creater */ | |
| NULL, /* dir merger --- default is to override */ | |
| NULL, /* server config */ | |
| NULL, /* merge server config */ | |
| auth_bearer_cmds, /* command apr_table_t */ | |
| register_hooks /* register hooks */ | |
| }; | |
| #endif /* HAVE_APU_JOSE */ | |