Skip to content

Commit

Permalink
Merge pull request #1192 from psteniusubi/feat-signed-jwks-verifier-jwks
Browse files Browse the repository at this point in the history
OIDCProviderSignedJwksUri and multiple verifier keys
  • Loading branch information
zandbelt authored Mar 7, 2024
2 parents 23ff660 + bc1a348 commit 64788fd
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 26 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
03/07/2024
- OIDCProviderSignedJwksUri: accept verification key set formatted as either JWK or JWKS
- properly handle parse errors in Require claim integer statements

03/06/2024
Expand Down
10 changes: 6 additions & 4 deletions auth_openidc.conf
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,21 @@
# Used when OIDCProviderMetadataURL is not defined or the metadata obtained from that URL does not set jwks_uri.
#OIDCProviderJwksUri <jwks_url>

# OpenID Connect Provider Signed JWKS URL (e.g. https://localhost:9031/pf/JWKS) followed by the JWK formatted
# key that can be used to verify the provided JWKs value.
# OpenID Connect Provider Signed JWKS URL (e.g. https://localhost:9031/pf/JWKS) followed by the verification key set
# formatted as either JWK or JWKS. The verification key set is used to verify the provided JWKs value.
# Specifying multiple keys allows the OP rotate the key used for signing the JWKs.
# I.e this is the URL on which the ID Token signing keys for this OP are hosted, in verifiable JWT formatting
# rather than relying on TLS for authentication and integrity protection.
# Used when OIDCProviderMetadataURL is not defined or the metadata obtained from that URL does not set signed_jwks_uri.
# When defined it takes precedence over OIDCProviderJwksUri
# Example:
# Examples:
# OIDCProviderSignedJwksUri https://localhost:9031/pf/JWKS "{\"kty\":\"oct\", \"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"}"
# OIDCProviderSignedJwksUri https://localhost:9031/pf/JWKS "{\"keys\":[{\"kty\":\"oct\", \"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"}]}"
# NB: for multi-OP setups:
# the 1st parameter is not used, it needs to be set anyhow (e.g. to "") if you wish to used the 2nd parameter
# the 2nd parameter is the default verification JWK for content pulled from the signed_jwks_uri for all providers and
# and its can be overridden with a per-provider key in the <issuer>.conf file using the key: signed_jwks_uri_key
#OIDCProviderSignedJwksUri <jwks_url> <jwk>
#OIDCProviderSignedJwksUri <jwks_url> [ <jwks> | <jwk> ]

# The fully qualified names of the files that contain the X.509 certificates with the RSA/EC public
# keys that can be used for ID Token verification.
Expand Down
35 changes: 28 additions & 7 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1303,10 +1303,31 @@ static const char *oidc_set_signed_jwks_uri(cmd_parms *cmd, void *m, const char
if (rv != NULL)
return OIDC_CONFIG_DIR_RV(cmd, rv);
}
cfg->provider.jwks_uri.jwk = oidc_jwk_parse(cmd->pool, arg2, &err);
if (cfg->provider.jwks_uri.jwk == NULL) {
return apr_psprintf(cmd->pool, "oidc_jwk_parse failed: %s", oidc_jose_e2s(cmd->pool, err));
json_error_t json_error;
json_t *json = json_loads(arg2, 0, &json_error);
if (json == NULL) {
return apr_psprintf(cmd->pool, "oidc_set_signed_jwks_uri failed: %s", json_error.text);
}
if (oidc_is_jwk(json)) {
oidc_jwk_t *jwk = NULL;
if (oidc_jwk_parse_json(cmd->pool, json, &jwk, &err) != TRUE) {
json_decref(json);
return apr_psprintf(cmd->pool, "oidc_set_signed_jwks_uri failed: %s",
oidc_jose_e2s(cmd->pool, err));
}
cfg->provider.jwks_uri.jwk_list = apr_array_make(cmd->pool, 1, sizeof(oidc_jwk_t *));
APR_ARRAY_PUSH(cfg->provider.jwks_uri.jwk_list, oidc_jwk_t *) = jwk;
} else if (oidc_is_jwks(json)) {
if (oidc_jwks_parse_json(cmd->pool, json, &cfg->provider.jwks_uri.jwk_list, &err) != TRUE) {
json_decref(json);
return apr_psprintf(cmd->pool, "oidc_set_signed_jwks_uri failed: %s",
oidc_jose_e2s(cmd->pool, err));
}
} else {
json_decref(json);
return apr_psprintf(cmd->pool, "oidc_set_signed_jwks_uri failed: invalid jwks argument");
}
json_decref(json);
return NULL;
}

Expand Down Expand Up @@ -1364,8 +1385,7 @@ char *oidc_cfg_dir_state_cookie_prefix(request_rec *r) {
}

static void oidc_cfg_provider_destroy(oidc_provider_t *provider) {
if (provider->jwks_uri.jwk)
oidc_jwk_destroy(provider->jwks_uri.jwk);
oidc_jwk_list_destroy(provider->jwks_uri.jwk_list);
oidc_jwk_list_destroy(provider->verify_public_keys);
oidc_jwk_list_destroy(provider->client_keys);
}
Expand Down Expand Up @@ -1397,7 +1417,7 @@ static void oidc_cfg_provider_init(oidc_provider_t *provider) {
provider->jwks_uri.uri = NULL;
provider->jwks_uri.refresh_interval = OIDC_DEFAULT_JWKS_REFRESH_INTERVAL;
provider->jwks_uri.signed_uri = NULL;
provider->jwks_uri.jwk = NULL;
provider->jwks_uri.jwk_list = NULL;
provider->verify_public_keys = NULL;
provider->backchannel_logout_supported = OIDC_CONFIG_POS_INT_UNSET;

Expand Down Expand Up @@ -1456,7 +1476,8 @@ static void oidc_merge_provider_config(apr_pool_t *pool, oidc_provider_t *dst, c
: base->jwks_uri.refresh_interval;
dst->jwks_uri.signed_uri =
add->jwks_uri.signed_uri != NULL ? add->jwks_uri.signed_uri : base->jwks_uri.signed_uri;
dst->jwks_uri.jwk = oidc_jwk_copy(pool, add->jwks_uri.jwk != NULL ? add->jwks_uri.jwk : base->jwks_uri.jwk);
dst->jwks_uri.jwk_list =
oidc_jwk_list_copy(pool, add->jwks_uri.jwk_list != NULL ? add->jwks_uri.jwk_list : base->jwks_uri.jwk_list);
dst->verify_public_keys = oidc_jwk_list_copy(pool, add->verify_public_keys != NULL ? add->verify_public_keys
: base->verify_public_keys);
dst->client_id = add->client_id != NULL ? add->client_id : base->client_id;
Expand Down
36 changes: 36 additions & 0 deletions src/jose.c
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,42 @@ apr_byte_t oidc_jwk_parse_json(apr_pool_t *pool, json_t *json, oidc_jwk_t **jwk,
return (*jwk != NULL);
}

apr_byte_t oidc_jwks_parse_json(apr_pool_t *pool, json_t *json, apr_array_header_t **jwk_list, oidc_jose_error_t *err) {
const json_t *keys = json_object_get(json, OIDC_JOSE_JWKS_KEYS_STR);
if ((keys == NULL) || (!json_is_array(keys))) {
oidc_jose_error(err, "JWKS did not contain \"" OIDC_JOSE_JWKS_KEYS_STR "\" array");
return FALSE;
}
*jwk_list = apr_array_make(pool, json_array_size(keys), sizeof(oidc_jwk_t *));
for (int i = 0; i < json_array_size(keys); i++) {
json_t *elem = json_array_get(keys, i);
if (elem == NULL)
continue;
oidc_jwk_t *jwk;
if (oidc_jwk_parse_json(pool, elem, &jwk, err) != TRUE) {
return FALSE;
}
APR_ARRAY_PUSH(*jwk_list, oidc_jwk_t *) = jwk;
}
return TRUE;
}

apr_byte_t oidc_is_jwk(json_t *json) {
const json_t *kty = json_object_get(json, OIDC_JOSE_JWK_KTY_STR);
if ((kty == NULL) || (!json_is_string(kty))) {
return FALSE;
}
return TRUE;
}

apr_byte_t oidc_is_jwks(json_t *json) {
const json_t *keys = json_object_get(json, OIDC_JOSE_JWKS_KEYS_STR);
if ((keys == NULL) || (!json_is_array(keys))) {
return FALSE;
}
return TRUE;
}

/*
* convert a JWK struct to a JSON string
*/
Expand Down
13 changes: 11 additions & 2 deletions src/jose.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
#define OIDC_JOSE_JWK_SIG_STR "sig" // use signature type
#define OIDC_JOSE_JWK_ENC_STR "enc" // use encryption type

/* the OIDC jwks fields from RFC 5741 */
#define OIDC_JOSE_JWKS_KEYS_STR "keys" // Array of JWKs

/* struct for returning errors to the caller */
typedef struct {
char source[OIDC_JOSE_ERROR_SOURCE_LENGTH];
Expand Down Expand Up @@ -171,11 +174,17 @@ typedef struct oidc_jwk_t {
/* decrypt a JWT */
apr_byte_t oidc_jwe_decrypt(apr_pool_t *pool, const char *input_json, apr_hash_t *keys, char **plaintext,
int *plaintext_len, oidc_jose_error_t *err, apr_byte_t import_must_succeed);
/* parse a JSON string to a JWK struct */
/* parse a JSON string (JWK) to a JWK struct */
oidc_jwk_t *oidc_jwk_parse(apr_pool_t *pool, const char *s_json, oidc_jose_error_t *err);
oidc_jwk_t *oidc_jwk_copy(apr_pool_t *pool, const oidc_jwk_t *jwk);
/* parse a JSON object in to a JWK struct */
/* parse a JSON object (JWK) in to a JWK struct */
apr_byte_t oidc_jwk_parse_json(apr_pool_t *pool, json_t *json, oidc_jwk_t **jwk, oidc_jose_error_t *err);
/* parse a JSON object (JWKS) to a list of JWK structs */
apr_byte_t oidc_jwks_parse_json(apr_pool_t *pool, json_t *json, apr_array_header_t **jwk_list, oidc_jose_error_t *err);
/* test if JSON object looks like JWK */
apr_byte_t oidc_is_jwk(json_t *json);
/* test if JSON object looks like JWKS */
apr_byte_t oidc_is_jwks(json_t *json);
/* convert a JWK struct to a JSON string */
apr_byte_t oidc_jwk_to_json(apr_pool_t *pool, const oidc_jwk_t *jwk, char **s_json, oidc_jose_error_t *err);
/* destroy resources allocated for a JWK struct */
Expand Down
49 changes: 37 additions & 12 deletions src/metadata.c
Original file line number Diff line number Diff line change
Expand Up @@ -575,12 +575,24 @@ static apr_byte_t oidc_metadata_jwks_retrieve_and_cache(request_rec *r, oidc_cfg
&cfg->outgoing_proxy, oidc_dir_cfg_pass_cookies(r), NULL, NULL, NULL) == FALSE)
return FALSE;

if ((jwks_uri->signed_uri != NULL) && (jwks_uri->jwk != NULL)) {
if ((jwks_uri->signed_uri != NULL) && (jwks_uri->jwk_list != NULL)) {

oidc_jwt_t *jwt = NULL;
oidc_jose_error_t err;
apr_hash_t *keys = apr_hash_make(r->pool);
apr_hash_set(keys, jwks_uri->jwk->kid ? jwks_uri->jwk->kid : "", APR_HASH_KEY_STRING, jwks_uri->jwk);

oidc_debug(r, "signed_jwks verifier keys count=%d", jwks_uri->jwk_list->nelts);
for (int i = 0; i < jwks_uri->jwk_list->nelts; i++) {
oidc_jwk_t *jwk = APR_ARRAY_IDX(jwks_uri->jwk_list, i, oidc_jwk_t *);
if (jwk->kid != NULL) {
oidc_debug(r, "signed_jwks verifier kid=%s", jwk->kid);
apr_hash_set(keys, jwk->kid, APR_HASH_KEY_STRING, jwk);
} else {
const char *kid = apr_psprintf(r->pool, "%d", apr_hash_count(keys));
oidc_debug(r, "signed_jwks verifier kid=%s", kid);
apr_hash_set(keys, kid, APR_HASH_KEY_STRING, jwk);
}
}

if (oidc_jwt_parse(r->pool, response, &jwt, keys, FALSE, &err) == FALSE) {
oidc_error(r, "parsing JWT failed: %s", oidc_jose_e2s(r->pool, err));
Expand Down Expand Up @@ -1117,6 +1129,28 @@ static void oidc_metadata_get_jwks(request_rec *r, json_t *json, apr_array_heade
}
}

static void oidc_metadata_get_signed_jwks_uri_key(request_rec *r, json_t *j_conf, apr_array_header_t **jwk_list,
apr_array_header_t *default_jwk_list) {
oidc_jose_error_t err;
json_t *json = json_object_get(j_conf, "signed_jwks_uri_key");
if (oidc_is_jwk(json)) {
oidc_jwk_t *jwk = NULL;
if (oidc_jwk_parse_json(r->pool, json, &jwk, &err) != TRUE) {
oidc_warn(r, "oidc_metadata_get_signed_jwks_uri_key failed: %s", oidc_jose_e2s(r->pool, err));
return;
}
*jwk_list = apr_array_make(r->pool, 1, sizeof(oidc_jwk_t *));
APR_ARRAY_PUSH(*jwk_list, oidc_jwk_t *) = jwk;
} else if (oidc_is_jwks(json)) {
if (oidc_jwks_parse_json(r->pool, json, jwk_list, &err) != TRUE) {
oidc_warn(r, "oidc_metadata_get_signed_jwks_uri_key failed: %s", oidc_jose_e2s(r->pool, err));
return;
}
} else if (default_jwk_list != NULL) {
*jwk_list = default_jwk_list;
}
}

/*
* parse the JSON conf metadata in to a oidc_provider_t struct
*/
Expand All @@ -1127,16 +1161,7 @@ apr_byte_t oidc_metadata_conf_parse(request_rec *r, oidc_cfg *cfg, json_t *j_con

oidc_metadata_get_jwks(r, j_conf, &provider->client_keys);

oidc_jose_error_t err;
json_t *jwk = json_object_get(j_conf, "signed_jwks_uri_key");
if (jwk != NULL) {
if (oidc_jwk_parse_json(r->pool, jwk, &provider->jwks_uri.jwk, &err) == FALSE) {
oidc_warn(r, "oidc_jwk_parse_json failed for \"signed_jwks_uri_key\": %s",
oidc_jose_e2s(r->pool, err));
}
} else if (cfg->provider.jwks_uri.jwk != NULL) {
provider->jwks_uri.jwk = cfg->provider.jwks_uri.jwk;
}
oidc_metadata_get_signed_jwks_uri_key(r, j_conf, &provider->jwks_uri.jwk_list, cfg->provider.jwks_uri.jwk_list);

/* get the (optional) signing & encryption settings for the id_token */
oidc_metadata_get_valid_string(r, j_conf, OIDC_METADATA_ID_TOKEN_SIGNED_RESPONSE_ALG,
Expand Down
2 changes: 1 addition & 1 deletion src/mod_auth_openidc.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ typedef struct oidc_jwks_uri_t {
char *uri;
int refresh_interval;
char *signed_uri;
oidc_jwk_t *jwk;
apr_array_header_t *jwk_list;
} oidc_jwks_uri_t;

typedef struct oidc_provider_t {
Expand Down

0 comments on commit 64788fd

Please sign in to comment.