mirror of
https://github.com/apache/httpd.git
synced 2025-08-01 16:41:19 +00:00

Or their non-zero value should be considered -1 rather than 1. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1916300 13f79535-47bb-0310-9956-ffa450edef68
1124 lines
36 KiB
C
1124 lines
36 KiB
C
/* 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);
|
|
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 */
|
|
|