Skip to content

Commit

Permalink
release 2.1.0: add updated AWS ALB JWKs retrieval
Browse files Browse the repository at this point in the history
supporting new "signer"/"region" logic and key rotation; closes:
OpenIDC/mod_oauth2#73

Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
  • Loading branch information
zandbelt committed Feb 12, 2025
1 parent bde2985 commit e9f7e92
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 24 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ reporting bugs, providing fixes, suggesting useful features or other:
Pavel Anpin <https://github.com/anpin>
smanolache <https://github.com/smanolache>
pladen <https://github.com/pladen>
Drew <https://github.com/drwxmrrs>
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
02/12/2025
- add updated AWS ALB JWKs retrieval supporting new "signer"/"region" logic and key rotation
closes: https://github.com/OpenIDC/mod_oauth2/issues/73
- release 2.1.0

01/02/2024
- update copyright year to 2025

Expand Down
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
AC_INIT([liboauth2],[2.1.0dev],[hans.zandbelt@openidc.com])
AC_INIT([liboauth2],[2.1.0],[hans.zandbelt@openidc.com])

AM_INIT_AUTOMAKE([foreign no-define subdir-objects])
AC_CONFIG_MACRO_DIR([m4])
Expand Down
2 changes: 1 addition & 1 deletion liboauth2_apache.pc.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ Name: liboauth2_apache
URL: https://github.com/OpenIDC/liboauth2
Description: Apache C bindings for liboauth2
Version: @VERSION@
Requires: liboauth2 >= 1.6.3, apr-1, apr-util-1
Requires: liboauth2 >= 2.0.0, apr-1, apr-util-1
Cflags: -I${includedir}
Libs: -L${libdir} -loauth2_apache
2 changes: 1 addition & 1 deletion liboauth2_nginx.pc.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ Name: liboauth2_nginx
URL: https://github.com/OpenIDC/liboauth2
Description: NGINX C bindings for liboauth2
Version: @VERSION@
Requires: liboauth2 >= 1.6.3
Requires: liboauth2 >= 2.0.0
Cflags: -I${includedir}
Libs: -L${libdir} -loauth2_nginx
2 changes: 2 additions & 0 deletions src/cfg/verify.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#define OAUTH2_JOSE_VERIFY_JWK_JWK_STR "jwk"
#define OAUTH2_JOSE_VERIFY_JWK_JWKS_URI_STR "jwks_uri"
#define OAUTH2_JOSE_VERIFY_JWK_ECKEY_URI_STR "eckey_uri"
#define OAUTH2_JOSE_VERIFY_JWK_AWS_ALB_STR "aws_alb"
#define OAUTH2_CFG_VERIFY_INTROSPECT_URL_STR "introspect"
#define OAUTH2_CFG_VERIFY_METADATA_URL_STR "metadata"

Expand All @@ -50,6 +51,7 @@ static oauth2_cfg_set_options_ctx_t _oauth2_cfg_verify_options_set[] = {
{ OAUTH2_JOSE_VERIFY_JWK_JWK_STR, oauth2_jose_verify_options_jwk_set_jwk },
{ OAUTH2_JOSE_VERIFY_JWK_JWKS_URI_STR, oauth2_jose_verify_options_jwk_set_jwks_uri },
{ OAUTH2_JOSE_VERIFY_JWK_ECKEY_URI_STR, oauth2_jose_verify_options_jwk_set_eckey_uri },
{ OAUTH2_JOSE_VERIFY_JWK_AWS_ALB_STR, oauth2_jose_verify_options_jwk_set_aws_alb },
{ OAUTH2_CFG_VERIFY_INTROSPECT_URL_STR, oauth2_verify_options_set_introspect_url },
{ OAUTH2_CFG_VERIFY_METADATA_URL_STR, oauth2_verify_options_set_metadata_url },
{ NULL, NULL }
Expand Down
168 changes: 156 additions & 12 deletions src/jose.c
Original file line number Diff line number Diff line change
Expand Up @@ -707,13 +707,15 @@ void oauth2_jose_jwk_list_free(oauth2_log_t *log, oauth2_jose_jwk_list_t *keys)

static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_list_resolve(oauth2_log_t *, oauth2_jose_jwks_provider_t *,
bool *);
bool *, cjose_header_t *);
static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_uri_resolve(oauth2_log_t *, oauth2_jose_jwks_provider_t *,
bool *);
bool *, cjose_header_t *);
static oauth2_jose_jwk_list_t *oauth2_jose_jwks_eckey_url_resolve(
oauth2_log_t *, oauth2_jose_jwks_provider_t *, bool *, cjose_header_t *);
static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_eckey_url_resolve(oauth2_log_t *,
oauth2_jose_jwks_provider_t *, bool *);
oauth2_jose_jwks_aws_alb_resolve(oauth2_log_t *, oauth2_jose_jwks_provider_t *,
bool *, cjose_header_t *);

static oauth2_jose_jwks_provider_t *
_oauth2_jose_jwks_provider_init(oauth2_log_t *log,
Expand All @@ -737,6 +739,12 @@ _oauth2_jose_jwks_provider_init(oauth2_log_t *log,
provider->jwks_uri = oauth2_uri_ctx_init(log);
provider->resolve = oauth2_jose_jwks_eckey_url_resolve;
break;
case OAUTH2_JOSE_JWKS_PROVIDER_AWS_ALB:
provider->jwks_uri = oauth2_uri_ctx_init(log);
provider->resolve = oauth2_jose_jwks_aws_alb_resolve;
provider->alb_arn = NULL;
provider->alb_base_url = NULL;
break;
}

return provider;
Expand Down Expand Up @@ -765,6 +773,11 @@ _oauth2_jose_jwks_provider_clone(oauth2_log_t *log,
case OAUTH2_JOSE_JWKS_PROVIDER_ECKEY_URI:
dst->jwks_uri = oauth2_uri_ctx_clone(log, src->jwks_uri);
break;
case OAUTH2_JOSE_JWKS_PROVIDER_AWS_ALB:
dst->jwks_uri = oauth2_uri_ctx_clone(log, src->jwks_uri);
dst->alb_arn = oauth2_strdup(src->alb_arn);
dst->alb_base_url = oauth2_strdup(src->alb_base_url);
break;
}

end:
Expand All @@ -790,6 +803,14 @@ _oauth2_jose_jwks_provider_free(oauth2_log_t *log,
if (provider->jwks_uri)
oauth2_uri_ctx_free(log, provider->jwks_uri);
break;
case OAUTH2_JOSE_JWKS_PROVIDER_AWS_ALB:
if (provider->jwks_uri)
oauth2_uri_ctx_free(log, provider->jwks_uri);
if (provider->alb_arn)
oauth2_mem_free(provider->alb_arn);
if (provider->alb_base_url)
oauth2_mem_free(provider->alb_base_url);
break;
}

oauth2_mem_free(provider);
Expand Down Expand Up @@ -1292,7 +1313,7 @@ bool oauth2_jose_jwt_verify(oauth2_log_t *log,
if (jwt_verify_ctx) {

keys = jwt_verify_ctx->jwks_provider->resolve(
log, jwt_verify_ctx->jwks_provider, &refresh);
log, jwt_verify_ctx->jwks_provider, &refresh, hdr);

ctx.jws = jws;
ctx.kid = cjose_header_get(hdr, "kid", &err);
Expand All @@ -1309,7 +1330,7 @@ bool oauth2_jose_jwt_verify(oauth2_log_t *log,
if (keys)
oauth2_jose_jwk_list_free(log, keys);
keys = jwt_verify_ctx->jwks_provider->resolve(
log, jwt_verify_ctx->jwks_provider, &refresh);
log, jwt_verify_ctx->jwks_provider, &refresh, hdr);
_oauth2_jose_verification_keys_loop(
log, keys, _oauth2_jose_jwt_verify_jwk, &ctx);

Expand Down Expand Up @@ -1846,8 +1867,46 @@ _OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_eckey_uri)
"eckey_uri");
}

static oauth2_jose_jwk_list_t *oauth2_jose_jwks_list_resolve(
oauth2_log_t *log, oauth2_jose_jwks_provider_t *provider, bool *refresh)
_OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_aws_alb)
{
char *rv = NULL;
oauth2_cfg_token_verify_t *verify = (oauth2_cfg_token_verify_t *)ctx;
const char *alb_base_url = NULL;

oauth2_debug(log, "enter");

rv = _oauth2_jose_verify_options_jwk_set_url(
log, value, params, verify, OAUTH2_JOSE_JWKS_PROVIDER_AWS_ALB,
"aws_alb");
if (rv != NULL)
goto end;

oauth2_jose_jwt_verify_ctx_t *ptr = verify->ctx->ptr;

// this is going to be set dynamically
if (ptr->jwks_provider->jwks_uri->endpoint->url) {
oauth2_mem_free(ptr->jwks_provider->jwks_uri->endpoint->url);
ptr->jwks_provider->jwks_uri->endpoint->url = NULL;
}

ptr->jwks_provider->alb_arn = oauth2_strdup(value);

alb_base_url = oauth2_nv_list_get(log, params, "alb_base_url");
if (alb_base_url) {
ptr->jwks_provider->alb_base_url = oauth2_strdup(alb_base_url);
}

end:

oauth2_debug(log, "leave: %s", rv);

return rv;
}

static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_list_resolve(oauth2_log_t *log,
oauth2_jose_jwks_provider_t *provider,
bool *refresh, cjose_header_t *hdr)
{
*refresh = false;
return oauth2_jose_jwk_list_clone(log, provider->jwks);
Expand Down Expand Up @@ -2171,22 +2230,107 @@ static oauth2_jose_jwk_list_t *_oauth2_jose_jwks_resolve_from_uri(
return dst;
}

static oauth2_jose_jwk_list_t *oauth2_jose_jwks_uri_resolve(
oauth2_log_t *log, oauth2_jose_jwks_provider_t *provider, bool *refresh)
static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_uri_resolve(oauth2_log_t *log,
oauth2_jose_jwks_provider_t *provider,
bool *refresh, cjose_header_t *hdr)
{
return _oauth2_jose_jwks_resolve_from_uri(
log, provider, refresh,
_oauth2_jose_jwks_uri_resolve_response_callback);
}

static oauth2_jose_jwk_list_t *oauth2_jose_jwks_eckey_url_resolve(
oauth2_log_t *log, oauth2_jose_jwks_provider_t *provider, bool *refresh)
static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_eckey_url_resolve(oauth2_log_t *log,
oauth2_jose_jwks_provider_t *provider,
bool *refresh, cjose_header_t *hdr)
{
return _oauth2_jose_jwks_resolve_from_uri(
log, provider, refresh,
_oauth2_jose_jwks_eckey_url_resolve_response_callback);
}

static const char *_oauth2_jose_jwks_aws_alb_region(const char *arn)
{
if (!arn)
return NULL;

char *arn_copy = oauth2_strdup(arn);
if (!arn_copy)
return NULL;

char *token = strtok(arn_copy, ":");
int count = 0;
const char *region = NULL;

while (token) {
if (count == 3) {
region = oauth2_strdup(token);
break;
}
token = strtok(NULL, ":");
count++;
}

oauth2_mem_free(arn_copy);
return region;
}

static oauth2_jose_jwk_list_t *
oauth2_jose_jwks_aws_alb_resolve(oauth2_log_t *log,
oauth2_jose_jwks_provider_t *provider,
bool *refresh, cjose_header_t *hdr)
{
cjose_err err;
char *url = NULL;
const char *region = NULL;

const char *signer = cjose_header_get(hdr, "signer", &err);
const char *kid = cjose_header_get(hdr, "kid", &err);

if (!signer || !kid) {
oauth2_error(log,
"missing 'signer' or 'kid' in JWT header: "
"signer=%s, kid=%s",
signer, kid);
return NULL;
}

if (strcmp(signer, provider->alb_arn) != 0) {
oauth2_error(
log,
"signer does not match configured ARN: signer=%s, arn=%s",
signer, provider->alb_arn);
return NULL;
}

if (provider->alb_base_url == NULL) {
region = _oauth2_jose_jwks_aws_alb_region(provider->alb_arn);
if (!region) {
oauth2_error(
log, "failed to extract region from ARN: arn=%s",
provider->alb_arn);
return NULL;
}
url = _oauth2_stradd4(NULL, "https://public-keys.auth.elb.",
region, ".amazonaws.com/", kid);
} else {
url = oauth2_stradd(NULL, provider->alb_base_url, kid, NULL);
}
oauth2_debug(log, "constructed ALB JWKs URL: %s", url);

provider->jwks_uri->endpoint->url = url;

oauth2_jose_jwk_list_t *result = _oauth2_jose_jwks_resolve_from_uri(
log, provider, refresh,
_oauth2_jose_jwks_eckey_url_resolve_response_callback);

provider->jwks_uri->endpoint->url = NULL;
oauth2_mem_free(url);

return result;
}

/*
oauth2_jose_jwk_list_t *
oauth2_jose_jwks_resolve(oauth2_log_t *log, oauth2_cfg_token_verify_t *verify,
Expand Down
25 changes: 18 additions & 7 deletions src/jose_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,33 @@ typedef struct oauth2_uri_ctx_t {
typedef enum oauth2_jose_jwks_provider_type_t {
OAUTH2_JOSE_JWKS_PROVIDER_LIST,
OAUTH2_JOSE_JWKS_PROVIDER_JWKS_URI,
OAUTH2_JOSE_JWKS_PROVIDER_ECKEY_URI
OAUTH2_JOSE_JWKS_PROVIDER_ECKEY_URI,
OAUTH2_JOSE_JWKS_PROVIDER_AWS_ALB
} oauth2_jose_jwks_provider_type_t;

typedef struct oauth2_jose_jwks_provider_t oauth2_jose_jwks_provider_t;

typedef oauth2_jose_jwk_list_t *(
oauth2_jose_jwks_resolve_cb_t)(oauth2_log_t *,
oauth2_jose_jwks_provider_t *, bool *);
oauth2_jose_jwks_provider_t *, bool *,
cjose_header_t *hdr);

typedef struct oauth2_jose_jwks_provider_t {
oauth2_jose_jwks_provider_type_t type;
oauth2_jose_jwks_resolve_cb_t *resolve;
union {
oauth2_uri_ctx_t *jwks_uri;
oauth2_jose_jwk_list_t *jwks;
};
// struct oauth2_jose_jwks_provider_t *next;

// NB: avoid union because of compiler/memory issues

// OAUTH2_JOSE_JWKS_PROVIDER_JWKS_URI and
// OAUTH2_JOSE_JWKS_PROVIDER_ECKEY_URI
oauth2_uri_ctx_t *jwks_uri;

// OAUTH2_JOSE_JWKS_PROVIDER_LIST
oauth2_jose_jwk_list_t *jwks;

// OAUTH2_JOSE_JWKS_PROVIDER_AWS_ALB
char *alb_arn;
char *alb_base_url;
} oauth2_jose_jwks_provider_t;

_OAUTH2_CFG_CTX_TYPE_START(oauth2_jose_jwt_verify_ctx)
Expand All @@ -88,6 +98,7 @@ _OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_pubkey);
_OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_jwk);
_OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_jwks_uri);
_OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_eckey_uri);
_OAUTH_CFG_CTX_CALLBACK(oauth2_jose_verify_options_jwk_set_aws_alb);

char *oauth2_jose_resolve_from_uri(oauth2_log_t *log, oauth2_uri_ctx_t *uri_ctx,
bool *refresh);
Expand Down
6 changes: 4 additions & 2 deletions test/check_jose.c
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ START_TEST(test_jwks_resolve_uri)
ck_assert_ptr_eq(rv, NULL);

ptr = (oauth2_jose_jwt_verify_ctx_t *)verify->ctx->ptr;
list = ptr->jwks_provider->resolve(_log, ptr->jwks_provider, &refresh);
list = ptr->jwks_provider->resolve(_log, ptr->jwks_provider, &refresh,
NULL);
ck_assert_ptr_ne(list, NULL);

oauth2_jose_jwk_list_free(_log, list);
Expand All @@ -390,7 +391,8 @@ START_TEST(test_jwk_resolve_plain)
ck_assert_ptr_eq(rv, NULL);

ptr = (oauth2_jose_jwt_verify_ctx_t *)verify->ctx->ptr;
list = ptr->jwks_provider->resolve(_log, ptr->jwks_provider, &refresh);
list = ptr->jwks_provider->resolve(_log, ptr->jwks_provider, &refresh,
NULL);
ck_assert_ptr_ne(list, NULL);

oauth2_jose_jwk_list_free(_log, list);
Expand Down
Loading

0 comments on commit e9f7e92

Please sign in to comment.