From c84ef0a9177e437abacd3870d710125fa74418dc Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 17:20:47 -0400 Subject: [PATCH 001/130] Additional test and log msg --- build.sh | 7 +++++++ src/ngx_http_auth_jwt_module.c | 2 ++ 2 files changed, 9 insertions(+) diff --git a/build.sh b/build.sh index c9f8047..f8d3242 100755 --- a/build.sh +++ b/build.sh @@ -31,3 +31,10 @@ if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then else echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; fi + +TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` +if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; +else + echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; +fi diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 5863b80..84878b8 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -109,6 +109,8 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // jwtcf->auth_jwt_key.data, // jwtcf->auth_jwt_enabled); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", r->headers_in.authorization); + // get the cookie // TODO: the cookie name could be passed in dynamicallly n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &jwtCookieName, &jwtCookieVal); From caee28d6b02e87d1d6387d2ce75a1181bcd792ce Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 17:28:09 -0400 Subject: [PATCH 002/130] test --- src/ngx_http_auth_jwt_module.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 84878b8..307829b 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -109,7 +109,8 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // jwtcf->auth_jwt_key.data, // jwtcf->auth_jwt_enabled); - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", r->headers_in.authorization); +// ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", r->headers_in.authorization); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header"); // get the cookie // TODO: the cookie name could be passed in dynamicallly From 049ba37b41f8bb325cc5d4b37cb28d1d6422135b Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 17:43:25 -0400 Subject: [PATCH 003/130] Added search headers function --- src/ngx_http_auth_jwt_module.c | 63 ++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 307829b..b0add55 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -23,6 +23,7 @@ static char * ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, voi static int hex_char_to_binary( char ch, char* ret ); static int hex_to_binary( const char* str, u_char* buf, int len ); static char * ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); +static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); static ngx_command_t ngx_http_auth_jwt_commands[] = { @@ -109,8 +110,16 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // jwtcf->auth_jwt_key.data, // jwtcf->auth_jwt_enabled); -// ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", r->headers_in.authorization); - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header"); + ngx_table_elt_t *h; + ngx_str_t authorizationHeaderName = ngx_string("Authorization"); + h = search_headers_in(&r, authorizationHeaderName.data, authorizationHeaderName.len); + if (h != NULL) + { + char* authvalue = ngx_str_t_to_char_ptr(r->pool, h.value); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", authvalue); + } + + // get the cookie // TODO: the cookie name could be passed in dynamicallly @@ -364,4 +373,54 @@ static char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str) return char_ptr; } +static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) +{ + ngx_list_part_t *part; + ngx_table_elt_t *h; + ngx_uint_t i; + + /* + Get the first part of the list. There is usual only one part. + */ + part = &r->headers_in.headers.part; + h = part->elts; + + /* Headers list array may consist of more than one part, + * so loop through all of it + */ + for (i = 0; /* void */ ; i++) + { + if (i >= part->nelts) + { + if (part->next == NULL) + { + /* The last part, search is done. */ + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + /* + * Just compare the lengths and then the names case insensitively. + */ + if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) + { + /* This header doesn't match. */ + continue; + } + + /* + * a-da, we got one! + * Note, we'v stop the search at the first matched header + * while more then one header may fit. + */ + return &h[i]; + } + + /* No headers was found */ + return NULL; +} From 8c643f27b89f81e536cfc6e7dc1bf891da894ed8 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 17:46:18 -0400 Subject: [PATCH 004/130] debug --- src/ngx_http_auth_jwt_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index b0add55..48a845c 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -112,7 +112,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_table_elt_t *h; ngx_str_t authorizationHeaderName = ngx_string("Authorization"); - h = search_headers_in(&r, authorizationHeaderName.data, authorizationHeaderName.len); + h = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); if (h != NULL) { char* authvalue = ngx_str_t_to_char_ptr(r->pool, h.value); From dba2163cb2cd2ffa9dafa265ebda5e782397ef4d Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 17:49:43 -0400 Subject: [PATCH 005/130] debug --- src/ngx_http_auth_jwt_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 48a845c..55fc208 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -115,7 +115,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) h = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); if (h != NULL) { - char* authvalue = ngx_str_t_to_char_ptr(r->pool, h.value); + char* authvalue = ngx_str_t_to_char_ptr(r->pool, h->value); ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", authvalue); } From 9aad8e0c8afe21f6891c7f2659172bf122474134 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 18:18:12 -0400 Subject: [PATCH 006/130] Check length and content of strings and fixed test --- build.sh | 6 +++++- src/ngx_http_auth_jwt_module.c | 34 ++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/build.sh b/build.sh index f8d3242..6be9eb8 100755 --- a/build.sh +++ b/build.sh @@ -32,7 +32,11 @@ else echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` +TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" +--cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. +eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. +eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; else diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 55fc208..365d944 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -98,6 +98,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_alg_t alg; time_t exp; time_t now; + int BEARER_LEN = 7; // strlen("Bearer "); jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); @@ -110,16 +111,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // jwtcf->auth_jwt_key.data, // jwtcf->auth_jwt_enabled); - ngx_table_elt_t *h; - ngx_str_t authorizationHeaderName = ngx_string("Authorization"); - h = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); - if (h != NULL) - { - char* authvalue = ngx_str_t_to_char_ptr(r->pool, h->value); - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", authvalue); - } - - // get the cookie // TODO: the cookie name could be passed in dynamicallly @@ -175,6 +166,29 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } + // if an Authorization header exists, it must match the cookie + ngx_table_elt_t *authorizationHeader; + ngx_str_t authorizationHeaderName = ngx_string("Authorization"); + authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); + if (authorizationHeader != NULL) + { + // compare lengths first + if (authorizationHeader->value.len != jwtCookieVal.len + BEARER_LEN) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match lengths"); + goto redirect; + } + + if (0 != strncmp(authorizationHeader->value.data + BEARER_LEN, jwtCookieVal.data)) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match content"); + goto redirect; + } + + char* authvalue = ngx_str_t_to_char_ptr(r->pool, authorizationHeader->value); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", authvalue); + } + return NGX_OK; redirect: From 3dbb129c0faef7e9eab022c043d191f3eddaa574 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 18:19:12 -0400 Subject: [PATCH 007/130] debug --- src/ngx_http_auth_jwt_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 365d944..7b05063 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -179,7 +179,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } - if (0 != strncmp(authorizationHeader->value.data + BEARER_LEN, jwtCookieVal.data)) + if (0 != strncmp(authorizationHeader->value.data + BEARER_LEN, jwtCookieVal.data, jwtCookieVal.len)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match content"); goto redirect; From db46b07781512c34ce715c7dc1da52f6ce91d4d8 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 18:21:27 -0400 Subject: [PATCH 008/130] debug --- src/ngx_http_auth_jwt_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 7b05063..4453f61 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -179,7 +179,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } - if (0 != strncmp(authorizationHeader->value.data + BEARER_LEN, jwtCookieVal.data, jwtCookieVal.len)) + if (0 != strncmp(authorizationHeader->value.data + BEARER_LEN, jwtCookieVal.data, jwtCookieVal.len)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match content"); goto redirect; From 1f38838f895927a61c7bb3dbd72039374b7ca481 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:17:22 -0400 Subject: [PATCH 009/130] strncmp doesn't work with u_char * --- src/ngx_http_auth_jwt_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 4453f61..c3f3bc1 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -179,7 +179,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } - if (0 != strncmp(authorizationHeader->value.data + BEARER_LEN, jwtCookieVal.data, jwtCookieVal.len)) + if (0 != strncmp((const char *)(authorizationHeader->value.data + BEARER_LEN), (const char *)jwtCookieVal.data, jwtCookieVal.len)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match content"); goto redirect; From dd5c1783dd201178391baebcb7441610360b77eb Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:24:16 -0400 Subject: [PATCH 010/130] Test had bad newlines --- build.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.sh b/build.sh index 6be9eb8..20d044f 100755 --- a/build.sh +++ b/build.sh @@ -32,11 +32,7 @@ else echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer -eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" ---cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. -eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. -eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` +TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" --cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; else From 8c77e171ae4b86957e309c8d0a4dfdb5dd508b8f Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:49:20 -0400 Subject: [PATCH 011/130] remove spaces from tests --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 20d044f..466ca7a 100755 --- a/build.sh +++ b/build.sh @@ -32,7 +32,7 @@ else echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" --cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` +TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" --cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; else From 0e01b3635d6e15156c0a46c4129347c4ab5d3347 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:49:33 -0400 Subject: [PATCH 012/130] clean up --- src/ngx_http_auth_jwt_module.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index c3f3bc1..fc483ef 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -85,9 +85,11 @@ ngx_module_t ngx_http_auth_jwt_module = { static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { + static const ngx_str_t jwtCookieName = ngx_string("rampartjwt"); + static const ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); + static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); + static const int BEARER_LEN = 7; // strlen("Bearer "); ngx_int_t n; - ngx_str_t jwtCookieName = ngx_string("rampartjwt"); - ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); ngx_str_t jwtCookieVal; char* jwtCookieValChrPtr; char* return_url; @@ -98,7 +100,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_alg_t alg; time_t exp; time_t now; - int BEARER_LEN = 7; // strlen("Bearer "); + ngx_table_elt_t *authorizationHeader; jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); @@ -167,8 +169,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } // if an Authorization header exists, it must match the cookie - ngx_table_elt_t *authorizationHeader; - ngx_str_t authorizationHeaderName = ngx_string("Authorization"); authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); if (authorizationHeader != NULL) { @@ -179,14 +179,12 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } + // compare content if (0 != strncmp((const char *)(authorizationHeader->value.data + BEARER_LEN), (const char *)jwtCookieVal.data, jwtCookieVal.len)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match content"); goto redirect; } - - char* authvalue = ngx_str_t_to_char_ptr(r->pool, authorizationHeader->value); - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "authorization header %s", authvalue); } return NGX_OK; From 060185de78a07e66c3ce737c0b151a6e0f0a45d5 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:54:57 -0400 Subject: [PATCH 013/130] didn't like the static const strings --- src/ngx_http_auth_jwt_module.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index fc483ef..d031d46 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -85,10 +85,11 @@ ngx_module_t ngx_http_auth_jwt_module = { static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { - static const ngx_str_t jwtCookieName = ngx_string("rampartjwt"); - static const ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); - static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); static const int BEARER_LEN = 7; // strlen("Bearer "); + + ngx_str_t jwtCookieName = ngx_string("rampartjwt"); + ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); + ngx_str_t authorizationHeaderName = ngx_string("Authorization"); ngx_int_t n; ngx_str_t jwtCookieVal; char* jwtCookieValChrPtr; From db01184205e816828cea064b5486c16e39a7906e Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:57:05 -0400 Subject: [PATCH 014/130] name test better --- build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 466ca7a..c6b3c6c 100755 --- a/build.sh +++ b/build.sh @@ -34,7 +34,7 @@ fi TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" --cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; + echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_EXPECT_200}${NONE}"; else - echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; + echo -e "${RED}Secure test with jwt and auth header fail ${TEST_SECURE_EXPECT_200}${NONE}"; fi From 358c168395534ec8bf4487301236d4574f851819 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 30 Oct 2017 19:59:25 -0400 Subject: [PATCH 015/130] pointing at branch to test --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3a8a83d..170d240 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ ARG TESLA_REPO_NAME=ngx-http-auth-jwt-module # ARG TESLA_REPO_URL_PREFIX=joefitz/ # ARG TESLA_REPO_FILE_PREFIX=joefitz- -# ARG TESLA_REPO_FILENAME=match-rh-nginx110-version +# ARG TESLA_REPO_FILENAME=validate-authorization-header ARG TESLA_REPO_URL_PREFIX= ARG TESLA_REPO_FILE_PREFIX= ARG TESLA_REPO_FILENAME=master From 9927fb944ef25f3aaeb80f78fdb2bcbd62cc343a Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 31 Oct 2017 13:21:30 -0400 Subject: [PATCH 016/130] Corrected comments --- src/ngx_http_auth_jwt_module.c | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index d031d46..dcc092e 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -392,15 +392,11 @@ static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, s ngx_table_elt_t *h; ngx_uint_t i; - /* - Get the first part of the list. There is usual only one part. - */ + // Get the first part of the list. There is usual only one part. part = &r->headers_in.headers.part; h = part->elts; - /* Headers list array may consist of more than one part, - * so loop through all of it - */ + // Headers list array may consist of more than one part, so loop through all of it for (i = 0; /* void */ ; i++) { if (i >= part->nelts) @@ -416,9 +412,7 @@ static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, s i = 0; } - /* - * Just compare the lengths and then the names case insensitively. - */ + //Just compare the lengths and then the names case insensitively. if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) { /* This header doesn't match. */ @@ -426,9 +420,9 @@ static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, s } /* - * a-da, we got one! - * Note, we'v stop the search at the first matched header - * while more then one header may fit. + * Ta-da, we got one! + * Note, we've stopped the search at the first matched header + * while more then one header may match. */ return &h[i]; } From 4ef1890ca1da49b151f09bd841d57c99939fb8e4 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 31 Oct 2017 13:43:26 -0400 Subject: [PATCH 017/130] Cleaner tests --- build.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index c6b3c6c..af912ae 100755 --- a/build.sh +++ b/build.sh @@ -11,6 +11,8 @@ RED='\033[01;31m' GREEN='\033[01;32m' NONE='\033[00m' +VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 + TEST_INSECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000` if [ "$TEST_INSECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Insecure test pass ${TEST_INSECURE_EXPECT_200}${NONE}"; @@ -25,14 +27,14 @@ else echo -e "${RED}Secure test without jwt fail ${TEST_SECURE_EXPECT_302}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` +TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALIDJWT}"` if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; else echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4" --cookie "rampartjwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4;PassportKey=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4"` +TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer ${VALIDJWT}" --cookie "rampartjwt=${VALIDJWT}"` if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_EXPECT_200}${NONE}"; else From 3852372579b947f48b46fc79dba1cd99f133b7f0 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 6 Nov 2017 20:43:18 -0500 Subject: [PATCH 018/130] use epel7 yum repo to get nginx and associated tweaks use epel7 yum repo to get nginx and associated tweaks --- Dockerfile | 33 +++++++++++++++++++++++++-------- resources/nginx.conf | 4 ++-- resources/nginx.repo | 5 ----- 3 files changed, 27 insertions(+), 15 deletions(-) delete mode 100644 resources/nginx.repo diff --git a/Dockerfile b/Dockerfile index 170d240..e697a98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,12 @@ FROM centos:7 LABEL maintainer="TeslaGov" email="developers@teslagov.com" -ARG NGINX_VERSION=1.12.0 - -COPY resources/nginx.repo /etc/yum.repos.d/nginx.repo +ARG NGINX_VERSION=1.10.2 ENV LD_LIBRARY_PATH=/usr/local/lib -RUN yum -y update && \ +RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ + yum -y update && \ yum -y groupinstall 'Development Tools' && \ yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake check-devel check && \ yum -y install nginx-$NGINX_VERSION @@ -16,6 +15,9 @@ RUN yum -y update && \ # for compiling for rh-nginx110 # yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed +# for compiling for epel7 +RUN yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed geoip geoip-devel google-perftools google-perftools-devel + RUN mkdir -p /root/dl WORKDIR /root/dl @@ -59,6 +61,7 @@ RUN unzip ${TESLA_REPO_FILENAME}.zip && \ # after 1.11.5 use this command # ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' +# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /etc/nginx/modules/. # build nginx module against nginx sources # # 1.10.2 from nginx by default use config flags... I had to add the -std=c99 and could not achieve "binary compatibility" @@ -66,15 +69,29 @@ RUN unzip ${TESLA_REPO_FILENAME}.zip && \ # # rh-nginx110 uses these config flags # ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/opt/rh/rh-nginx110/root/usr/share/nginx --sbin-path=/opt/rh/rh-nginx110/root/usr/sbin/nginx --modules-path=/opt/rh/rh-nginx110/root/usr/lib64/nginx/modules --conf-path=/etc/opt/rh/rh-nginx110/nginx/nginx.conf --error-log-path=/var/opt/rh/rh-nginx110/log/nginx/error.log --http-log-path=/var/opt/rh/rh-nginx110/log/nginx/access.log --http-client-body-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/scgi --pid-path=/var/opt/rh/rh-nginx110/run/nginx/nginx.pid --lock-path=/var/opt/rh/rh-nginx110/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=c99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' +# +# epel7 version uses these config flags +# ./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' +# +#RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ +# tar -xzf nginx-$NGINX_VERSION.tar.gz && \ +# rm nginx-$NGINX_VERSION.tar.gz && \ +# ln -sf nginx-$NGINX_VERSION nginx && \ +# cd /root/dl/nginx && \ +# ./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ +# make modules && \ +# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. + +# ARG CACHEBUST=1 RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ tar -xzf nginx-$NGINX_VERSION.tar.gz && \ rm nginx-$NGINX_VERSION.tar.gz && \ ln -sf nginx-$NGINX_VERSION nginx && \ - cd /root/dl/nginx && \ - ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' && \ - make modules && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /etc/nginx/modules/. + cd /root/dl/nginx && \ + ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ + make modules && \ + cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. # Get nginx ready to run COPY resources/nginx.conf /etc/nginx/nginx.conf diff --git a/resources/nginx.conf b/resources/nginx.conf index cef43ce..5bfb041 100644 --- a/resources/nginx.conf +++ b/resources/nginx.conf @@ -5,7 +5,7 @@ worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; -load_module modules/ngx_http_auth_jwt_module.so; +load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; events { worker_connections 1024; @@ -32,4 +32,4 @@ http { include /etc/nginx/conf.d/*.conf; } -daemon off; \ No newline at end of file +daemon off; diff --git a/resources/nginx.repo b/resources/nginx.repo deleted file mode 100644 index c8393c9..0000000 --- a/resources/nginx.repo +++ /dev/null @@ -1,5 +0,0 @@ -[nginx] -name=nginx repo -baseurl=http://nginx.org/packages/centos/7/x86_64/ -gpgcheck=0 -enabled=1 From 1439114af94bc573637b8292e5b0266b69c9fb3e Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 13 Nov 2017 18:49:12 -0500 Subject: [PATCH 019/130] optionally redirect or return unauthorized --- resources/test-jwt-nginx.conf | 1 + src/ngx_http_auth_jwt_module.c | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index ab79472..8129f6b 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -2,6 +2,7 @@ server { auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; auth_jwt_loginurl "https://teslagov.com"; auth_jwt_enabled off; + auth_jwt_redirect on; listen 8000; server_name localhost; diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index dcc092e..9435765 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -14,6 +14,7 @@ typedef struct { ngx_str_t auth_jwt_loginurl; ngx_str_t auth_jwt_key; ngx_flag_t auth_jwt_enabled; + ngx_flag_t auth_jwt_redirect; } ngx_http_auth_jwt_loc_conf_t; static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf); @@ -48,6 +49,13 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_enabled), NULL }, + { ngx_string("auth_jwt_redirect"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_redirect), + NULL }, + ngx_null_command }; @@ -272,7 +280,14 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; } - return NGX_HTTP_MOVED_TEMPORARILY; + if (jwtcf->auth_jwt_redirect) + { + return NGX_HTTP_MOVED_TEMPORARILY; + } + else + { + return NGX_HTTP_UNAUTHORIZED; + } } @@ -308,6 +323,7 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) // set the flag to unset conf->auth_jwt_enabled = (ngx_flag_t) -1; + conf->auth_jwt_redirect = (ngx_flag_t) -1; ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); @@ -324,11 +340,15 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); - if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) { conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; } + + if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) + { + conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; + } ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Merged Location Configuration"); From 10bd80d6c99f5ac139e8bbeee151d751a7eca7b5 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 13 Nov 2017 20:44:48 -0500 Subject: [PATCH 020/130] Testing no redirect --- build.sh | 23 +++++++++++++++-------- resources/test-jwt-nginx.conf | 6 ++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/build.sh b/build.sh index af912ae..1af4b45 100755 --- a/build.sh +++ b/build.sh @@ -27,16 +27,23 @@ else echo -e "${RED}Secure test without jwt fail ${TEST_SECURE_EXPECT_302}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALIDJWT}"` -if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_EXPECT_200}${NONE}"; +TEST_SECURE_COOKIE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALIDJWT}"` +if [ "$TEST_SECURE_COOKIE_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; else - echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_EXPECT_200}${NONE}"; + echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; fi -TEST_SECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer ${VALIDJWT}" --cookie "rampartjwt=${VALIDJWT}"` -if [ "$TEST_SECURE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_EXPECT_200}${NONE}"; +TEST_SECURE_HEADER_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer ${VALIDJWT}" --cookie "rampartjwt=${VALIDJWT}"` +if [ "$TEST_SECURE_HEADER_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; else - echo -e "${RED}Secure test with jwt and auth header fail ${TEST_SECURE_EXPECT_200}${NONE}"; + echo -e "${RED}Secure test with jwt and auth header fail ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; +fi + +TEST_SECURE_NO_REDIRECT_EXPECT_401=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-no-redirect/index.html` +if [ "$TEST_SECURE_NO_REDIRECT_EXPECT_401" -eq "401" ];then + echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; +else + echo -e "${RED}Secure test with jwt and auth header fail ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; fi diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index 8129f6b..4bd5847 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -7,6 +7,12 @@ server { listen 8000; server_name localhost; + location ~ ^/secure-no-redirect/ { + auth_jwt_enabled on; + auth_jwt_redirect off; + alias /usr/share/nginx/secure/; + } + location ~ ^/secure/ { auth_jwt_enabled on; root /usr/share/nginx; From 20026b68ae0573483d25c6cc803cc227a52dd9be Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 13 Nov 2017 21:34:31 -0500 Subject: [PATCH 021/130] fix text for test --- build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 1af4b45..4f04a74 100755 --- a/build.sh +++ b/build.sh @@ -43,7 +43,7 @@ fi TEST_SECURE_NO_REDIRECT_EXPECT_401=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-no-redirect/index.html` if [ "$TEST_SECURE_NO_REDIRECT_EXPECT_401" -eq "401" ];then - echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; + echo -e "${GREEN}Secure test without jwt no redirect pass ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; else - echo -e "${RED}Secure test with jwt and auth header fail ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; + echo -e "${RED}Secure test without jwt no redirect fail ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; fi From 85e2a1cbf06f22b24d431c989747ea871250cf26 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Fri, 1 Dec 2017 15:44:23 -0500 Subject: [PATCH 022/130] EPEL upgraded nginx from 1.10.2 to 1.12.2 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e697a98..83ae747 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM centos:7 LABEL maintainer="TeslaGov" email="developers@teslagov.com" -ARG NGINX_VERSION=1.10.2 +ARG NGINX_VERSION=1.12.2 ENV LD_LIBRARY_PATH=/usr/local/lib From 629149b64d90e98ef1d9a34a7eba63d40a39af65 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 16 Jan 2018 15:47:01 -0500 Subject: [PATCH 023/130] Log userid and email in nginx access logs. --- Dockerfile | 19 +++++---- resources/nginx.conf | 18 ++++++--- src/ngx_http_auth_jwt_module.c | 74 ++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 83ae747..d5854ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,17 +47,19 @@ RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ # get our JWT module # change this to get a specific version? -ARG TESLA_REPO_NAME=ngx-http-auth-jwt-module +#ARG TESLA_REPO_NAME=ngx-http-auth-jwt-module # ARG TESLA_REPO_URL_PREFIX=joefitz/ # ARG TESLA_REPO_FILE_PREFIX=joefitz- # ARG TESLA_REPO_FILENAME=validate-authorization-header -ARG TESLA_REPO_URL_PREFIX= -ARG TESLA_REPO_FILE_PREFIX= -ARG TESLA_REPO_FILENAME=master -ADD https://github.com/TeslaGov/$TESLA_REPO_NAME/archive/${TESLA_REPO_URL_PREFIX}${TESLA_REPO_FILENAME}.zip . -RUN unzip ${TESLA_REPO_FILENAME}.zip && \ - rm ${TESLA_REPO_FILENAME}.zip && \ - ln -sf ${TESLA_REPO_NAME}-${TESLA_REPO_FILE_PREFIX}${TESLA_REPO_FILENAME} ${TESLA_REPO_NAME} +#ARG TESLA_REPO_URL_PREFIX= +#ARG TESLA_REPO_FILE_PREFIX= +#ARG TESLA_REPO_FILENAME=master +#ADD https://github.com/TeslaGov/$TESLA_REPO_NAME/archive/${TESLA_REPO_URL_PREFIX}${TESLA_REPO_FILENAME}.zip . +#RUN unzip ${TESLA_REPO_FILENAME}.zip && \ +# rm ${TESLA_REPO_FILENAME}.zip && \ +# ln -sf ${TESLA_REPO_NAME}-${TESLA_REPO_FILE_PREFIX}${TESLA_REPO_FILENAME} ${TESLA_REPO_NAME} + +ADD . /root/dl/ngx-http-auth-jwt-module # after 1.11.5 use this command # ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' @@ -99,5 +101,6 @@ COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure ENTRYPOINT ["/usr/sbin/nginx"] +#ENTRYPOINT ["while true; do echo hello world; sleep 1; done"] EXPOSE 8000 diff --git a/resources/nginx.conf b/resources/nginx.conf index 5bfb041..7ea8afb 100644 --- a/resources/nginx.conf +++ b/resources/nginx.conf @@ -2,7 +2,7 @@ user nginx; worker_processes 1; -error_log /var/log/nginx/error.log warn; +error_log /var/log/nginx/error.log info; pid /var/run/nginx.pid; load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; @@ -16,11 +16,14 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + log_format upstream_time '$remote_addr $sent_http_x_userid [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt="$request_time" uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time" ' + '$sent_http_x_email'; - access_log /var/log/nginx/access.log main; + access_log /var/log/nginx/access.log upstream_time; sendfile on; #tcp_nopush on; @@ -29,6 +32,11 @@ http { #gzip on; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Server $remote_addr; + include /etc/nginx/conf.d/*.conf; } diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 9435765..a750f40 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -24,7 +24,9 @@ static char * ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, voi static int hex_char_to_binary( char ch, char* ret ); static int hex_to_binary( const char* str, u_char* buf, int len ); static char * ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); +static ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr); static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); +static ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); static ngx_command_t ngx_http_auth_jwt_commands[] = { @@ -98,6 +100,8 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_str_t jwtCookieName = ngx_string("rampartjwt"); ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); ngx_str_t authorizationHeaderName = ngx_string("Authorization"); + ngx_str_t useridHeaderName = ngx_string("x-userid"); + ngx_str_t emailHeaderName = ngx_string("x-email"); ngx_int_t n; ngx_str_t jwtCookieVal; char* jwtCookieValChrPtr; @@ -107,6 +111,10 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_t *jwt; int jwtParseReturnCode; jwt_alg_t alg; + const char* sub; + const char* email; + ngx_str_t sub_t; + ngx_str_t email_t; time_t exp; time_t now; ngx_table_elt_t *authorizationHeader; @@ -196,6 +204,23 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } } + // extract the userid + sub = jwt_get_grant(jwt, "sub"); + if (sub == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain a subject"); + } + sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); + set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); + + email = jwt_get_grant(jwt, "emailAddress"); + if (email == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); + } + email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); + set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); + return NGX_OK; redirect: @@ -406,6 +431,22 @@ static char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str) return char_ptr; } +/** copies a character pointer string to an nginx string structure */ +static ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr) +{ + int len = strlen(char_ptr); + + ngx_str_t str_t; + str_t.data = ngx_palloc(pool, len); + ngx_memcpy(str_t.data, char_ptr, len); + str_t.len = len; + return str_t; +} + +/** + * Sample code from nginx. + * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/?highlight=http%20settings + */ static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) { ngx_list_part_t *part; @@ -451,3 +492,36 @@ static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, s return NULL; } +/** + * Sample code from nginx + * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/#how-can-i-set-a-header + */ +static ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) { + ngx_table_elt_t *h; + + /* + All we have to do is just to allocate the header... + */ + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + return NGX_ERROR; + } + + /* + ... setup the header key ... + */ + h->key = *key; + + /* + ... and the value. + */ + h->value = *value; + + /* + Mark the header as not deleted. + */ + h->hash = 1; + + return NGX_OK; +} + From 203e43e5899ec339c03c4268fec4057eecd11111 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Fri, 26 Jan 2018 13:01:41 -0500 Subject: [PATCH 024/130] copy so out of container --- build.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sh b/build.sh index 4f04a74..5515201 100755 --- a/build.sh +++ b/build.sh @@ -7,6 +7,8 @@ CONTAINER_ID=$(docker run --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${D MACHINE_IP=`docker-machine ip` +docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . + RED='\033[01;31m' GREEN='\033[01;32m' NONE='\033[00m' From a0a4bfaa173df70f9efcdc2083766e108ff0f069 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Wed, 31 Jan 2018 11:28:03 -0500 Subject: [PATCH 025/130] removed authorization header check removed authorization header check --- build.sh | 7 ---- src/ngx_http_auth_jwt_module.c | 72 ---------------------------------- 2 files changed, 79 deletions(-) diff --git a/build.sh b/build.sh index 5515201..6da50d7 100755 --- a/build.sh +++ b/build.sh @@ -36,13 +36,6 @@ else echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; fi -TEST_SECURE_HEADER_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer ${VALIDJWT}" --cookie "rampartjwt=${VALIDJWT}"` -if [ "$TEST_SECURE_HEADER_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt and auth header pass ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; -else - echo -e "${RED}Secure test with jwt and auth header fail ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; -fi - TEST_SECURE_NO_REDIRECT_EXPECT_401=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-no-redirect/index.html` if [ "$TEST_SECURE_NO_REDIRECT_EXPECT_401" -eq "401" ];then echo -e "${GREEN}Secure test without jwt no redirect pass ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index a750f40..a548996 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -25,7 +25,6 @@ static int hex_char_to_binary( char ch, char* ret ); static int hex_to_binary( const char* str, u_char* buf, int len ); static char * ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); static ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr); -static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); static ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); static ngx_command_t ngx_http_auth_jwt_commands[] = { @@ -95,11 +94,8 @@ ngx_module_t ngx_http_auth_jwt_module = { static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { - static const int BEARER_LEN = 7; // strlen("Bearer "); - ngx_str_t jwtCookieName = ngx_string("rampartjwt"); ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); - ngx_str_t authorizationHeaderName = ngx_string("Authorization"); ngx_str_t useridHeaderName = ngx_string("x-userid"); ngx_str_t emailHeaderName = ngx_string("x-email"); ngx_int_t n; @@ -117,7 +113,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_str_t email_t; time_t exp; time_t now; - ngx_table_elt_t *authorizationHeader; jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); @@ -184,25 +179,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt has expired"); goto redirect; } - - // if an Authorization header exists, it must match the cookie - authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); - if (authorizationHeader != NULL) - { - // compare lengths first - if (authorizationHeader->value.len != jwtCookieVal.len + BEARER_LEN) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match lengths"); - goto redirect; - } - - // compare content - if (0 != strncmp((const char *)(authorizationHeader->value.data + BEARER_LEN), (const char *)jwtCookieVal.data, jwtCookieVal.len)) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization and Cookie do not match content"); - goto redirect; - } - } // extract the userid sub = jwt_get_grant(jwt, "sub"); @@ -443,54 +419,6 @@ static ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr) return str_t; } -/** - * Sample code from nginx. - * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/?highlight=http%20settings - */ -static ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) -{ - ngx_list_part_t *part; - ngx_table_elt_t *h; - ngx_uint_t i; - - // Get the first part of the list. There is usual only one part. - part = &r->headers_in.headers.part; - h = part->elts; - - // Headers list array may consist of more than one part, so loop through all of it - for (i = 0; /* void */ ; i++) - { - if (i >= part->nelts) - { - if (part->next == NULL) - { - /* The last part, search is done. */ - break; - } - - part = part->next; - h = part->elts; - i = 0; - } - - //Just compare the lengths and then the names case insensitively. - if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) - { - /* This header doesn't match. */ - continue; - } - - /* - * Ta-da, we got one! - * Note, we've stopped the search at the first matched header - * while more then one header may match. - */ - return &h[i]; - } - - /* No headers was found */ - return NULL; -} /** * Sample code from nginx From c861b1e227360e3821ba183e13ebb2634886c236 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Fri, 2 Feb 2018 10:43:02 -0500 Subject: [PATCH 026/130] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96b4973 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Tesla Government + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 6c6077676b4fa9ba2964a9079ce15e02a0ca5f3d Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 5 Feb 2018 04:24:26 -0500 Subject: [PATCH 027/130] Joefitz/optional auth header (#24) * created more tests, exit if docker fails * copy files to support more tests * Added cookie vs auth header feature, refactored code --- Dockerfile | 2 + build.sh | 44 +++-- config | 2 +- ngx_http_auth_jwt_module.so | Bin 0 -> 177360 bytes resources/test-jwt-nginx.conf | 12 +- src/ngx_http_auth_jwt_binary_converters.c | 49 +++++ src/ngx_http_auth_jwt_binary_converters.h | 18 ++ src/ngx_http_auth_jwt_header_processing.c | 95 ++++++++++ src/ngx_http_auth_jwt_header_processing.h | 14 ++ src/ngx_http_auth_jwt_module.c | 213 ++++++++-------------- src/ngx_http_auth_jwt_string.c | 32 ++++ src/ngx_http_auth_jwt_string.h | 18 ++ 12 files changed, 347 insertions(+), 152 deletions(-) create mode 100755 ngx_http_auth_jwt_module.so create mode 100644 src/ngx_http_auth_jwt_binary_converters.c create mode 100644 src/ngx_http_auth_jwt_binary_converters.h create mode 100644 src/ngx_http_auth_jwt_header_processing.c create mode 100644 src/ngx_http_auth_jwt_header_processing.h create mode 100644 src/ngx_http_auth_jwt_string.c create mode 100644 src/ngx_http_auth_jwt_string.h diff --git a/Dockerfile b/Dockerfile index d5854ed..4d61a23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,6 +99,8 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ COPY resources/nginx.conf /etc/nginx/nginx.conf COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure +RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header +RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect ENTRYPOINT ["/usr/sbin/nginx"] #ENTRYPOINT ["while true; do echo hello world; sleep 1; done"] diff --git a/build.sh b/build.sh index 6da50d7..019e89a 100755 --- a/build.sh +++ b/build.sh @@ -1,42 +1,62 @@ #!/bin/bash +RED='\033[01;31m' +GREEN='\033[01;32m' +NONE='\033[00m' + # build DOCKER_IMAGE_NAME=jwt-nginx docker build -t ${DOCKER_IMAGE_NAME} . +if [ $? -ne 0 ] +then + echo -e "${RED}Build Failed${NONE}"; + exit 1; +fi + CONTAINER_ID=$(docker run --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${DOCKER_IMAGE_NAME}) MACHINE_IP=`docker-machine ip` docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . -RED='\033[01;31m' -GREEN='\033[01;32m' -NONE='\033[00m' - VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 -TEST_INSECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000` +TEST_INSECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000 -H 'cache-control: no-cache'` if [ "$TEST_INSECURE_EXPECT_200" -eq "200" ];then echo -e "${GREEN}Insecure test pass ${TEST_INSECURE_EXPECT_200}${NONE}"; else echo -e "${RED}Insecure test fail ${TEST_INSECURE_EXPECT_200}${NONE}"; fi -TEST_SECURE_EXPECT_302=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html` -if [ "$TEST_SECURE_EXPECT_302" -eq "302" ];then - echo -e "${GREEN}Secure test without jwt pass ${TEST_SECURE_EXPECT_302}${NONE}"; +TEST_SECURE_COOKIE_EXPECT_302=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache'` +if [ "$TEST_SECURE_COOKIE_EXPECT_302" -eq "302" ];then + echo -e "${GREEN}Secure test without jwt cookie pass ${TEST_SECURE_COOKIE_EXPECT_302}${NONE}"; else - echo -e "${RED}Secure test without jwt fail ${TEST_SECURE_EXPECT_302}${NONE}"; + echo -e "${RED}Secure test without jwt cookie fail ${TEST_SECURE_COOKIE_EXPECT_302}${NONE}"; fi TEST_SECURE_COOKIE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALIDJWT}"` if [ "$TEST_SECURE_COOKIE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt pass ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; + echo -e "${GREEN}Secure test with jwt cookie pass ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; +else + echo -e "${RED}Secure test with jwt cookie fail ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; +fi + +TEST_SECURE_HEADER_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-auth-header/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer ${VALIDJWT}"` +if [ "$TEST_SECURE_HEADER_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with jwt auth header pass ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; +else + echo -e "${RED}Secure test with jwt auth header fail ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; +fi + +TEST_SECURE_HEADER_EXPECT_302=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-auth-header/index.html -H 'cache-control: no-cache'` +if [ "$TEST_SECURE_HEADER_EXPECT_302" -eq "200" ];then + echo -e "${GREEN}Secure test without jwt auth header pass ${TEST_SECURE_HEADER_EXPECT_302}${NONE}"; else - echo -e "${RED}Secure test with jwt fail ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; + echo -e "${RED}Secure test without jwt auth header fail ${TEST_SECURE_HEADER_EXPECT_302}${NONE}"; fi -TEST_SECURE_NO_REDIRECT_EXPECT_401=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-no-redirect/index.html` +TEST_SECURE_NO_REDIRECT_EXPECT_401=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-no-redirect/index.html -H 'cache-control: no-cache'` if [ "$TEST_SECURE_NO_REDIRECT_EXPECT_401" -eq "401" ];then echo -e "${GREEN}Secure test without jwt no redirect pass ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; else diff --git a/config b/config index 17c6762..317aeea 100644 --- a/config +++ b/config @@ -2,7 +2,7 @@ ngx_addon_name=ngx_http_auth_jwt_module ngx_module_type=HTTP ngx_module_name=ngx_http_auth_jwt_module -ngx_module_srcs="$ngx_addon_dir/src/ngx_http_auth_jwt_module.c" +ngx_module_srcs="$ngx_addon_dir/src/ngx_http_auth_jwt_binary_converters.c $ngx_addon_dir/src/ngx_http_auth_jwt_header_processing.c $ngx_addon_dir/src/ngx_http_auth_jwt_string.c $ngx_addon_dir/src/ngx_http_auth_jwt_module.c" ngx_module_libs="-ljansson -ljwt" . auto/module diff --git a/ngx_http_auth_jwt_module.so b/ngx_http_auth_jwt_module.so new file mode 100755 index 0000000000000000000000000000000000000000..6125fcaeabe4ccf751860dbcaed1a637ff216ebe GIT binary patch literal 177360 zcmeFad3Y36_CH)zNjf)BBu&=tBqRkLNCP2sAWNr#kgzroAZ!{HBq0d|WyyqqQBhGA zi7|?fqqvQ@jDpLE>)?(nxQ)0Dj*ey=ok?_D$5B+2-{*6xIz&dk-{12-?;r2;KGT}2 zv)yygJ@?#mZ{1tnoHx00ilOUT6l;crJCG2X48-M@$^rI36qxzY-f_B!SEI^`mFEn=o~S5ivTqH*Wn zepdVo_Z*w7Meda4r|P4Ydcyy-i2dii-X?32d!*bPl5Kp=$=^oov)cnTNss~ zqNDY#^;$G}gYh>Uf9Yx8#7>y}=06|)+vV$T-+tTtJ5mz9%xin{!N7(-53IQAtKaQ@ z{=i=k{;E3jp^h)(KYso5_|T8{4%(gh!T5Q9d1^k2ANLcb#c5Wg!H!fW?$$4q=zyWe zh>Fxe(47$dS*bIQBYz?s^-J=7LH{NFd!Y4~t!#mP(+-KBQ?+fQEQey>`eIj%j%Vm5M+A9SFddm`4wpC;-5!S|BwIKC3UP!K9ihb<4Mbx_tQ|;E%{E3dY>LD;OR)cT#}RxAmesDMRSnd(k~Y zu`fG%(KC9{clM(587lJG-HYCPT_)isXkGU(`(!!hfw#0P3 z2y%GiGwNx(rXxicWLesg!JBYR$Swy&yphcA&Lj8?K8rYqRrdkGn~5`}bng}XG~ygu z-Ma-}LYzaPdxzjB6Xy`_-X{25;v5p)8w8(DoMGSHB=`j490J`nf`^DR^t-DBA4QxY z-(4>FFyajL?ttKfh%>~yEy2@?GlaX{f+rG>C$0(Jhd4v9`-`7I7;F${=yi7r{`~;p zOmW=@1pkUSL$G_V;2#rb=ymTF`~%_)t?nIy|Cu;Ls(YK@uMuY`b#D;-1>y{$?k2&X zBF@n1t`YoU;!LI8Rf6A3oGG-sT=3h8Go-o$g5N|ulei`LHN+W0-EQFFj+hz?vCuJY zE^Z6LLl1@9J_>I<6n}=X=xGE>3#1t+UoAS)$#{VLP+NXbFZm}AMTOg*3P1Vr_^^H; z{OX~#QaEL?6n+hb!)?zU`GDm&jO#||X=`)C8^=ur8&$S_)MBFk@4c4RH4Yx{YQ$yH zb1{1%sQ>F}mRg#hki7{eGgQazhoIIn1jc=vX&aZm0Ojum8M=-fSk!$Y6eMJCpnUfX zz(#CAaGUzv zWLEx+D%g9!flgsPHf8?n_oM3!N!mE%8nH|v6UV5Ij&bf4!8X) z-1cK-M@y>iv_IkTbF|8~&%zs@*25d0HM-0n;hM+KIa1m7_byG~Njb4kc1HJ^l;f%_ z{nyrsN7lYnus7UZnWO5%qfM!KZ893}K4nqG!iq%|r#`(09zF8OyIlo`j~v-napJVL zKUJJCt?e5zIoz?jXZVUT;g0B2!q7jnqdrEPS-L()wV>-^%$P;0`Gau# zN_Tkkr!A$G?JKo{y#?jf$s-E#B9Dcl5hgikC~W5AQ4YvkMDeL;pMh27;{XANE{15MrJ%G6CM3i*Ruxo4SkE*hzg&34n*Ap&ZE$tPL6(?4l;0)%r zAHwJVb32Ok8q%9`wu(_mt1n{?4DXwi6QG#{$~g&g)Ca-cLoVPj(q31Z1*;IgQ~j0g zg$EF(2%RI-HlB+qd((UvIjwz4U|RbL=+j9#H4sBn=jSwr+ncJw8_S{+F3jZsX(w;< z-h@pnQ34siU&5xTHlz}^yh*5?g*wKag{mL2ZF%cowBI0jc4(Di7QSZ&g|Fre63_&38sa{rS(ZOM9iH$T+c`r?Ap_h^vion>g=InRAPdpy(P#*B1h?P}P;1-w3{>d(tl&u3 zWV#sPHM1jcDyGA(2H?Vmn4P*=pQmd8=*Z4pMXaZ8MUHz~+rDr+jY4R4wD=QX9b>Cf z|EW?(#&7a;rK1R)pk8f^hS=+p7F(%t^-IE)iG{bsVxbC~Q zO8BA89o9cb?FhuR%`3)r)u9k(U3X=BPR_%jk!(jxj;hdKnO3*2V&lFj6rEOgWT{i^ zgXhYPkzFHEh8^UY*fBP@vhCw=-4RjxTvrP1w@nSVHCo*xIMp-LO=H^bSfAssZ2N!> zf3>@-^lN);T}E&HETC@T+}6?WN;LRr-=7B6-4}r)`oP`=Zy#mPAp3N%?Qu}qJ{fC( zev5u4lS`Q#n6z};<6lc(zu&bHRyY=Shei7a*aL%LYnKbmjfeCWWX^dQ`^|P9+Y)Wt z*9Z1hwCz1=!#;?2{{u;J>9+O&yYBqIbBgI&4f^EvbKr!v8074$iu37udB_^PMw>W$_Kazh$7$SDZ>YCgR$JKZt+#3|Yy|fr%BHi>makfe zoqkBHSO)bijVrC?tH`hh*J}-}ORI3RF|qa+D$_N)3y7%%Eu#n;U9d z(7s5;)fg4;OQArV`^~cBs2f>BS+TbUWgFC1@|{_{~GsA2-C3$ z++>Wsi*awk{ZZU+!Tm$rKg2y5Bj9VKB%Zy_M>e02`z^S);2zl%($AW!>8)-(!;Dqi zP%a0$Gx2xevm-~o0ItQmr^I`vC-gl-ZP3Q2j~O;PXAr1bCH|0hwXI(sIdVA(74hyZ z#-w=9#;9<-)zT;4Gcn#>5wB+SoedaFz*Bz?N60KKx~{lo9 zBx-WJ)e$`*-hVOmY;jGF4{nT|0%a%mi4RT^eWr-G22jsMmFP4!%TB^!D6T+jFeszn}kl;J+UDuLu6? zf&Y5ozaIFn2mb%-fynzEk@qoUtR^mNeB7}OCZD}r`{Cot(ZwCA0iz4?|7hdB3Q=RU=`pWxh=I`?(XeT#Fy(z)O2 z+#huA&pG$EocrIM`!~+r<-EL*>fDDo_hXv>e~qI5e)c(D!9l;WdvJ$kOGMs_io6FE zozSr@}`Hv|V8Sjzz!H#EV;Fxm%PtgO`|6FqF#EBuxk8dyx zwF>it`9;>KKtWNUc+_aiKNs6w;o26PKBAy_sJv)q=P^62MP6c_9m!V5)WyvKLrX$V zw&f!)Nq=;VTm-e{({@JWBQHa*hzJ~Wx8>iwFCrfaqMPjMj*+%EgbeM>WqQx_@pF*` zSQ;%PrCVr|{x&YRqs70}P$LaYBP$5`C|!+#RFo0>E^pDXeZY!Ru`#^qu5rSPnFdLt z2=k8NUkNZeI)*R1MyZ%@$rwS##VrI2Aa8UKgpP%{#VD8BK~#55BF{^`14n>jTsjn* z-k3?Iisft!eP02KWu7`1^Gzk>)v3AA@hvOW)b$|59Om}6QgE~u^CMxm;t4>^PlP>c zGi8ntw&FHXM$?J-n8B0wVndX^9)mdXn_B!D)?q8SRZp)5inZ z5Z6L6k3JvB1#vtBit*~(aN8Jn5R-Du0G(6t*0`bM4AHkhY+KytL`Le{fozZahDbn= z9dSPrDG+35Tr^dW5@dHAzc~;yTE7=%_QaJ;08%8j?Tx#FWlHp0aeGGV^DyPEjXevP zC+0>rQYDgf6G2U7;~H}_o2yhDcCun_V{6@-OU(n#rCtQqcLy#s+q?__sq9XnJ{X}U zf%+(remg{9A3-`KJ~}SoM|}yWcpZ!l(FwN`Qwa}1VuL$j5s6kpH?h5%%s3l1 z=n3q_m!!=4pd!X3O)tl~OLn0%FLBfxI z-Qy7GU)Ko!4xs?U@f%Qv)+6{!9O1kW7+I6;Mv0`y!O;~}UVz?C9_7e%a-1fQbFjn2 z+|R-)J(;3=9s1pX`O&Yt84~^R;y488x-9(lU$S;7(9x)615v(!Zx`ocHia+i>uKbk zguhgL%|O$=JD|+CY)Trx)~2W1*wmEO_4S&b$%t7%5qH~M2R{e zo*ZWxSro(iwL|QO?4)JPMv>T^jGK)62<{Sa6bjPx;lj8Y9NIqJ`=QB1IWj5{yyCMaZtk zO+x>%AHdrL{Jsp}0|I8v2e^lzOYtjOkqn(i)1MZRdP+Yh=Jd+j_oG2kgE&x)i_wax zQ5;4F-@1>gex2Z96pLD!M6X^8vFI5ek;-%*^M*92gtE^tq>Kwa0H0-Tv3u~QqC2|v z4R%=GWRyFL006sg5WG%KN2q9P2cM&0%WearIh-W z+$2WmMEoYF9ikua@tWPdLPvDjMk6Sj&i0Qbt4-o(~i1{pgrxVo7r4@+EG2z z&L2QS5JQ=CHqDmKCY9YO)bt3I(~boso`K<#{OnQ%AS5s%Cj)TzT)>|JLjOd}U%0yz zdo`(^GIRk5CjpLKh;ylJarwhzVB3ZBP+{O~a+Ng^aM5)H)jB+H$Blaouv@L03%HE) zut%-U1YFKZJyX?|0Is+Uu%%9e;<$-4DM!VR06dvG{c2SL;OXqnfVvpNByQG^fP*Tf z5O9@(edX##JaWgKayQ_xx&uQnZXw}n^(PG7xJ85;)$QqkPov4LS{z5$7=Yq7Lo@D9 z2As(-iM?(#Ebe>d>1^A^NfkJaK><(dc{rN!QlhcdVMnl$0>tneK# z^=NTy$8`}w6lA~N8Ns8qWGg=BMW87+VtB+4iA5-ojsyf_fBp9PFVm59{T zrKFyK=^`Ob{L+EmNJtmI{H+S$0Q8DxJd5B@kYpK~T!g@I&%T`_`-Ew*a*hE|Q#WE8 z%RTp>(5%!1Uj#PQ%k1_O{tCKVZATJvpZFHwM0G(3a5d$V)m!K<_sP`bQ70z>K83KQ zCcpys!fzqtS5?%xnDl@uLnykJuxwDhMS3IQv6}IwhijUC5HjN6{lri&z z;bNmN=7z*7p+1OCOG@}FWMg*$+@Jhj29|kB&w@qo?*$nd1@_e~A)EXGhpk(U z%LV))=^izS@JEC#wHwtZ|AVk!^+B7GyO=BjsdX)_TJpyn=t1@MBG5ni63=tx>N;4H z{7=e@+}6W*Z`2>(cUr^+Dw6XD$| zm+%q7d(6>ZM%qxV8CP4dv;QAqTUrOeCL?voO02Qf!g#D}bGty& z#Gq6^jZji?>M&O9R&Py!%m03`H0!~(!2*JAwaP$!}G)FA1*)jrx5N=16z zqaH@0PaQ+J({mUON*za7>7Mn-`l)4H><4sD-WVX`IfBBv=V_W(u@hoVx@VOe$OIxg zbYl$v0hvssOZWVl&6vt&e4%^#!9l5E6UZUmbKg85(}-w>XEv>$!ERKB zhy9g0b1_|Ict+&{nMK4hJlhI@%q9{rJij4QMWo#DoE8Eyhe#C^(5G{W)L_P#RD_;N zY(?F%r2wnaO8-PV>b11cYB)zrU0K_zrH$btz*76l(V=76Wqy@_q?I=AEWiQPglk&a zFkFMG2uriH@n6H(a_QEza&~K2GZyBdrlb#8Qx!nBXAJu|m5)T8vCpNUE|mo5ctj^A zl1`+?q~t`%#J&Zv$~)>R!0IH#y>~PnsHu1iN^jw9bc}i!1KL|$2#>i{9s0r>+z6XJ z>c{zjLvtZxsS(iQ9mAORtJkLk9=9I!fSNP|aG4$JHC2G8FpkT`9&gyO|5J?gq?DUz z&tCxgQ%-u1_Ajm1QcivoO>~CgDfFyTN$9|oMg1@|JW_4SsZ?)iMkA73QffEV6ruZr zDKpFZMaoJ~YLcf?X0wUzRD91^OR4&H1@#oyK<6CNEj7#lJeROveGJE^%%lE*W&}q; zf7&$Yj2!{+K)QP$V49iU|EQViiME+HAkfp3I$^R$RHmo!-S3_1E0jr3CA?dDG2KHi z>`AR%zFJEk^p6Z=Bk$ELJD77FCE$UM2IZX_4bXebTcN~s9SmONdr37MiZ3x_ekm) z(`_^!=vbgLY_y;9n&`cZnKo)F?^}q}jMHtjuadCNSoK9B!=3YH##+XLM>;Fx40e{K z&dvq>Oy(!QN)7{F&#YI4$J*iO8t+Qj8oLhQ{){VM25fiDRnj%K{_WJSWSC`Ky%)y2 z8Alm6FpfN_cr8-Pxak(eh^3N>p#FBQQv7Nto-Q-)VAM5g#@~=IlhO~es@DK^WuEy# zf2XRVW}$f+!3rwJqS-_ZirdMs-~kqGS5E=uwPw_L7#cS zNWcNr2foSNSPwWTxjb|8z9tUhFZ+UiAwxMV-pOpEcdBIm$!upbtCkU$*|8EaS);gc z&b*YX;zl(ejmX^Q!&qxlJU7U^jKii?4Vw-4*MzsKJ@9Pi63HSo^geY8(Axx!O6inf&D~5IG@;Ymfq={0ZKLs>UQagAdu_B|r}rkLz|8w>)a>*w83*)!L5)70 z-Zr8S*r@9C<{%I=|6mKpc522hu#+-B?T?_i0ib6iX3aru z(Rx<7mwh^5wv7%b_nw207*J)SndRP$F+k_oXhymBuT(qNM$^kR<9DObqU;|~jZ5h; zB1ytu=6;vrFfx4LC0PT&b15GsxMXni0s@Dw@zw%tpDjTIX~qn6RMu(J!OUPV8GnRr z*>1*?QIDo)CpUl^+lnQfKRfkdz^QyyIXi7OKs@i~LBvC{Qnm1VwwJJ5)u4QKI$@9c z2JOktsFZ=vbe^3_cpz*!8GK_Y#&fpyBEZ;_0D7_q9|o)zmVi8j8Z`APCi?7L!b)w1 zC$jw%LGKn10x~ET-R@HTa7`M7i<^Q+FPiZ>BGq!!V51MF`hopdLTRRZkozP^r*c!VE{)HmR1dznVl`Tc(y+mD# z%Q52`Oj>DK+*vW^PXuT^jRLV106l4gxC)e+B+YjhK)XBAa(*Bvi>tK36yXuX9^8$K zp)qY}3=(bZ*#O;X!?~ukCG!YsYVQn z7cZoUTirYoGV&ncL2*8l7+niMDh`0{Qe68Qe`V0hK9TVjsGh_tagBWmmnu&(XTj8_ z)oV0QirqV`)st%16f=p<=2YWW-;?ekj0GU_fhV0ZmRRP=AnZ?Vsa>XdGB-eTKqgtw z0ICnF%Ss`i#Y9!^4I{ukPJCsDy=xH|9={!KSz+&-X+VeD=zy^IK0I-FM%ZX(*xLsK z)e~7Gg}q#pdPdsZ^sx62Q-KC-)Eo8=%?0YLpgdvk0NOCh=B9r(A#R;Jri@hLqf0uz{q?RfS~lz2w67y(6-UV@AfLJIdx zjqFM+9}s5C141f(Gs#oMXAY&xhC+G{VYeE5BH+1%Ju**u=G&HKTe2$k zEU?im%lj9WI>AN!4|Szb;P>8!Dl zy&I5)gy%HIS5RHTo<5!JELS7YD9;KuKWz8(O7?WMYKD6}tJ$_1b^36?O$=wOl|IY^ zGA$YvxKtlp6R3yvNie$EKFwG)8A3CLiqJ@0lg3j(E({(NJ%n4P-_vpt7^(c(D9>6c zDs!P{9m)3NlIIMWN9+c)vH>2&Db8~y8)iWj%NgG=0?(R^V#X|t_QCF30me=M=pLN- zBDKAW3^h23pgmXzCkq)0xU|Xi4{^)m^L@}Zq(2+0se1`0GN&j>a6^(95E!6{;33JR zd&I6GDYVPdj1#a>7?S!b^chD$$#v&JGWK(Tp4`MmfECr}CLL9uD{AaSlbb>jCYph` zX9jb1&aZ?6nio=;9vMIFuT4*w9Q^c_)q23q-;MdChSoI zF&p`7B;jesRT$X*+M7ia9|w(B=41XHaYzV1;hMk+zh5Q{`9YkcFecLT$v4(l036Aw zCbK4e)ON_o##j1i)?-im>4kO_ErS2jizwnzze9>lFZmQ20_rUc|MVc$2bGo!xYPp} zL-jS|r^rDGqVyRIqE6L{wO@KAcN@A?)G)xaSlJg|?sBBtD@f%z ziy8i1_lM%k}p`F&D!fLr>GI>2-7V7$B$1Igm2>z&dn=aNH)B4?|Z9$78;QiifU} z@|L-}KakZT<~I+I2GXQ|49x-4i!3|zS5hVjV?F{gK8R`%#{gZ5a9J>0#FVsFEr!~2iN zLQ7LuBHIp6BCIr{3jP_MGKI3`!##iKkxeJ)$Yu&zMI}O1QUEt(R%*kuxasdvry&A{ z=dj1vF6uQFVF5dQa5GdG8&N9H%_oJ}H2^(%iP0De>N2<{FNqtvcCyS%7P2JKycE)z zruL&)rj_TN$RfMY>b!K;qp6i+0cQ|a>M<+^@@%`*bTlU~i*yfO*O><-yAR4{?#~;P zZb6uS$n()9n)-7T;KBA#c^_8i4I%7NXZr#BX^thSF>fegzs$9H!)Q`Ky;2UDJgN+< zM_H$A&sNDACvPN&O|`6X@&bfw)QcE?d7~af*+%sPdNQw&^qs1Lbrmre?)F}Z$j>Wb zr$_5)yJa-ym2x!hk%e%cjLg083cX=$u7dQKS!|p&VtO?!k!SW1Gx)5n$us+iN#gF>5&F5jnUTFF-!k+X6855r9>x10Dk`)u(2$dM$NL<4P?xTbAVN z0?cx$7Da+GpQjF_Ou4$u2l^m-K8)=*jJMQ`{Sb1gKjJEL_y+*oauKaAwOsh0$@Ocw zlHpREk6p?&kk_}M(C80BzCR6wGy|Y!WMtdM-bl~iE~|R= z1EMqkTCO2HiaX8uzh&;WRKJ;kZxDOct}?(kazDFDy#U4eH}Mg!QLV!hL;fwKZ&Pzo zD*tz+?^N%N0(=|k2h?lWHp|~hdY9*+$$)R?Z12`Rr;~995li>n%G&NEf*teobAkMx zNV)F02$tpFO{7Zq_>l1OcN3}6J^d#FxsMh$>7F~O`hHf~s(W6H0`dUs+@^bC!$2Nn znLWB^19EQuABgPNJ@+6n`r+M%YU2Z+XF2B&kVUe zDhLhv?{L=Ls}dL`e_`4E>TK5g9^nIaw7t)0JE$2?j0YohI~3uR9@ixL!7a|fq93lx z^yiPGjWR0p%Z`r9{P9OeWq!GgN;PjTv{i7GWU0wW@g z@t7-rD$8khxiDqjcDZSU6V*P(#B{>RnlYdP$c+9dn;FcX^>~jCIV5p(RQ1v^r>y(EW%;%8`AyP8; z8*`-=IeaxnO9uhp`ipb|w5ELN`W0#vYf{&0`D>qtRgE$s=bypU-DHPqD@!)pp?4e^y0e?uRb z+^Y7_{0m6ls$?i`X2fr^qkjwG?Wrv-E42Izc|^Y>y``>6%Wt!1hU{Cs-(aH1Z@1B` zTkK4B2|Hn@ywH)qjUBaHGg7g*%D-$Rs>Gauwjrm&a+hKV#16q4#~L|d7wGa_I#S-& zu=D=NN%NxNKQ#&?YUJdV@TFTlItB0)!pZ7;%w{8}4u=e8*}r;#glnM3s4Yk5&wB!3 zELU8fg3k{FR>NRU!51uQpXwBRNjO@jn1Zjk`caAtuYyA@pkt1D3WWpnsmEB4ssalH z9E-6SI6=T=nEV1K3Ydr4G*B(zU1(|GBmtkY0G=%1Ye+kRQv~b`09+{Gca&Wu;MZ_R z;8X!S;O@X;0soBkSfED0(-#1&74Y#B04@>m{tSS1Y>9De2w=T{-@|2rWly0su?15R zcm=PeV2V#&vq~#?o#*mcp+`WKx@vj7R`3SPxos!CNjTYd%v)TLdThu1iF8Xd>I$K~ z)q=+C4-k%?QKkAyAXdFLDx`0lOre(5P1>lj@@A|uPamjhqsjy^tyK-nv{B;)amzw? zRJkBX5iemv3!K>sRaw8r$Tecv;H3whv&uGLrj)J0vlYAfobOn{lM~sn zMf!-Ls9;?~vo`8fow2T2wasY2VtYMq)h*FRHR!iO7XqfV6vfXAqC{+fQ8{|%a=_x# z(X+U!v*UX7Y(8TsIn5niB`ZJ8_&wWtehcKY&xgCJ3rqCj7+@AVr!Z(Y&OAIx(+c5i zXo@yZK*Sf05k#3oMgXz>ZAzmG%bbUV3nqY5uK#Nq3^ETxb>T#v@4HwgPlpSq=y^c= zrWZ9AhDBSztVd84PS-2J37QkpD}|MUl$!(Nfy~mIzzLhXFtQ7)#1mE63CKeWE+`&` z7IZ^uXW=ut3mL{d!aAST*P)1J&RYoNIekYM4lqANoEPqwI^AYL9FXS)Nw!umU#Atm zpr4O29*gjc0{bnVP#3DZ( zBGzgy!+N3cH4)ojet<43d|i-D=7Up!ydm{$)r<*nbzozEv?lfr1V?M(Ew2HVld8hs zu{Sg|7DKY|R_Phlfq_^kZ`RovrEn)I#o&^z*!5?$-J@<}ZMT01*ius29ZBdRzxr?n z=ywJI2juv^@Gi;+)lw|63-4YEdbxU*e!u4mz+v^f(SYygYf)87!s7wjP_LvvA0*tU zve;>R_CUS~3vEn9flYsb{n-=PV})+L6nFDYHlV-$IHa6zN_4u3NwF~52`2;XFH8~t zdCV3zG);dVWh_%#iIUd=A!n`-N|qD!exlQ)@)wh@j_ zZPJP^y9=?VQkQ8(mlLv6WYMo%!1t*2Sbr5=LAs^NQC-n*2>T^n7F|gl0re=BVMSMs zN52MBf29>&O^Nc<)3l;%2!+)&4CkEcQG7T1 zqFQ`fyqokI@oDir^l78`wD?{ZywfT^Exw;V-JlsWG1LOv($I$3-&rVCyos-Ii%*L; z+di#;k*jcXV>*6cu>#*X%fAN(X?5tAFNMyy=QodRnIG29E{0Z8rGLeW&Ha(6N zhxomuje2aqmt17~oiS9>{t0|y54VyI8E&bow318cK^&84C6^NN<5i@RZG-~op%NJx zjOh{?8Rcr;FlhRg(MgmB@CMbN@b!eZs$?XClAGv>ZR(GNZ~g}K?dnCsw-DZ;8Rub027diKS|9s% zB5EmFaBhLqV~Kge^4=S)-(; zKlJ;h_ez#b1{{#yD`_|xa8P=$WEtUdyIsrKuCVl8$?3GYN-aS?FKPKTWUAGZ=+% zZZhYfQ-az0mq1$0(eXemJrRAk!MqNh2@cc?fNZr^)oH;&dI{id=19z_XT{}6ovpc^3m6U>kdOE`#XTG!DsZBG48E8{GM3wIlTt$c3i%%x6AiCE*CG` zs~C?gJc2JcE|=E7B(1ls<@HTk@MV1|wE4{^N1%Rtu@W$U7zX5klqoc~vBj_H?JzTF z@{26N*99rJTl}W9IBYJQ3(lYPUqM@qd3`yMgZddj8qM$e19@9!YfSwwVM`=%{XsM* z>nfxT<9uig+`t!6Vi%$x{K5MQ3gLw3rl8RM=R-nM%T2(tq)@6F(HQ*0g`m6DmIAOg0ynm@?6;VZwv|3S2XmkroTO=`k5a#;o-W6J?Q{~h3S>ZRSNbN0RI5xh zrEk%GOw3=yae{q!p6!FNlLdP+a1yi(Uq0Q{8~(~4WY z0>71heKR`Nqh4jb-?T&CvfKVG+wNCXR{Gr!C>yZ-aG3T4Z9n`(Ka`u|2Ti9V!lpRF z(CLXPQ#=u^vq-fmu5jt}MU5%GP&%E_Xo@rX==4UDSww65N|{zuTx06=%?49^(@&>! zw#vgsX}pMSlf+bNf4OG6`2w3|C#LJn1hlR+MQ0;-nD=8PQkte;4+T5TC^)XvD+6P< zsk50Gg4}QNmBG?XLH3yUV;GkX(CM+g=5vVZ(riKYn|p@?8K_?m)d$RnkR3{WQs$s} zGCOpzJl?%)*2DtI)qevYbV_P0^-F5(!Z)#?A#jfeb!VMjjk%SW zeSsFbE9Q=+jF1-wceM&?kZ|)O}wgDTF?Yxd-m7(S1Xv0Q)lL4~x+oy6>AH zuy11C0oJ7F@LL(7?_>B{UaRi=JOJJi(Xv7J4abZb(p@cR&{o~Y#X-n$F_Uf6ebva& zp=j6Ruw@6@M;FD&7k+o@zWiapVqNGfZMW_V(l2p>?a_S?l>_S|I``^6els=HSIX_z zeRT}geuBNE`wr8C@h*nV0o}KU1F6639kl46?%Pc}lUxkLPTj{J`wJzzxSZVgxk#aSL?;}R)IM+YG zYczb9qH{uJ!fP^oeB&}S-X(o&_|j-~SZd#3_#S1rPjev_w5^73DH3aFrmL(5eQWr} zQRi%ziQ2atzRO1fJ3;zkhv9pheOoQqErzdx@p-afI}P9UBY@S3U+#n_SZ=x0x7+Z& zg?TP?nn>=!7aX2KxCZV!2`O?=E#hQ{5o(QER*uoJ%YxjQQA~@PJ&wKMG*b0~#ZXlZl37W$g7wu6@ z%;hFuVhdds)dD1J_T?zMHtG#%t1|yZhh7);1(0fUGh2IoR5D_|#(bq5$lXzWz-g2v zb?6?^c7^!?#qN*FhuC)WW=sd62cj6MJ4_G5{n4mGaCVvxk@JLbcAFzOoSuq037kD< zJ%)Se*{D_^d(9i@xaXs;0{+nQyGl)MF zfBAJg@%=rDi7%l0BzgZsToKfLlD$8UV)icAeUiTaDNPPLsqQn8ta1|O=TRK)^PPnG zMHCZeweAZexOPs^eiEJAoD>orO`TWhzDF4u zeWLkHyIuF4O}{2Yr!AHQ=FI@+j%GLQ)P0?FSz%^9$WK4ojHO!00yc4(Yx`47`v}c$(qc$-$o+ z%>LkS$;cau0;#%QtM`BnEvjYz_ zmt$4jRED4#)BkX^gpF1Dqo>0(GYPI68z)a`c%pe|0m?rWz!v-x$-QVVcRUv#$}a)Smt&lvoiY>CP#Ml%Vr73Z}DHzESoJ4 zumST^^hQ~g{uBGobPokGN04&K#AS1(OxS!9Jzq9Yb|9+EHM4=t7o=JeY1sntO^ufQ zFneU&P-(3BclZr34Q_ucU-N0E%szrb$AeN9(q&MZ9~J`{qu&Rb+x!zE zw`{Eb8W4}km&nV;>Hh>`nNw&(nQ;7aoK!YmcO!BF=8Rl$%JsoOf~Lm@q(YE#lVi4Q zf<7Ibu;Ec#(7hbyqN(C5)AfW?t4gK z1#aU;1WNAh5H090GPxrM0ZufyotJwNTu_i?aL+9F<}|>`#$|-xz@oe$#o)G6E?GCMS;iIMR?;(z+U5S!gG)&3(}1T2;WYtGmJ+F{|R}wAk%n~ za5wc2F!m9St^%B8yiE9Ewk+Fto$!@Gz?ShgU_ZZ1S}@qQ{|R(e!4MlR2eTm8cpo=7 z|H@om{~*`a za1@wEKV*0r;065*hKXkKz2ySGaVDgc)^|)Yu%X6BAi5NH(zCxTVpw>icqk_+6tEo_ z$*Qou71W{Zzru^zeM6hq)M|y72(_NzW#>|$zELZ@ViTc;I<4@kO94SWk1t$`hes~O z8U_)*g8rDej|5 z+sFdy48*dCG!J!;l|EX|kzNsz=8>*#nvCk+L;D)AT z82`U<7G5MsWNR(V93-8^6`C=~(65Qf#vuh#+d17>I!J{gif>%eRd8Qt!Ydnt%UFsY$n`cd8cK=EgJ}E_34ZLI$OG=;^IiafXTKX*9bS0S ziq9v61@AvGYZYIx3;cx7aN(c3C-&W61BE^ocPSo~yVRS2rHNTHAxZ(a^sJz~K!&<(_916VB| zhDF@f1T}RxW{Ti7LWbyRn0Ji#F);_P%>&z|O7mD2TTxc8T{@@2Iq6COYt?V z*j7vdR&ejFJwkhJA!A%3)V**I=&>P8RaWq&7kXq~iYWRE9)|EERhVjHe}zbI41Tq2 zHfrNueehpgF5qji{Q!SWSjmsr1pmzwE4;WfAM|gi)1%(>0{)go@pYtFz=ufp%NKWp z-yHxPP@kfz;P+f=1XVoN3&9@|e`oJ+9<#=qgl z;7@M>%svY->kqp0mr=s>3$Dd7v%{=Kq0YYB0R~O|TyXI2s$7qh6aF06?90*X*4QXHk(GHJaKw+_LO8Y|mjdtd zkXHI@?7I=R2PBu`Q*Jh2aJI%)MDfJd)>7fD4qR#+XxV3E!{V_iQ9Q*puZHy4)TldG zpeL;6`sERXUoJTn0up*W)s@iWFOax$oe=v5!p16Xd#8udW;2XE z&|Z4O-$BocM?}P445+;Hwz|2{zz4h1om{kOYCEE${tw97x4WjitL;6P-fSj6>KdDZS?t zfG)*GxD^+?u@WK^CE14934DH#8NOK#Dh^=2N~exfFO4Bl=LVkqP>i4ZqD+k)Mhit zP;e1BJlN&m&o~!Im1lsAwFuIfOTAx?aGD0A%Qx$HvmBl9GRyTm@Y;Tng=5CcR{DVs~xo%bJENqHY%px3A8xhtOvk5n<U z^Av38nM;sgLi?pmt9fP!$n$ytoDJqE_#pIxeln1)Cg1A~y{MlBWSenx?L2T?azePNRUeTWiXSdmjzzQAEyMgR6A3(kcy{dB;cCUGw z2gqys*Wm0o?^i6#fkWQ`d2k6((8T#+g3)#gUcHqXI`3%QIi zv~9DGCgNn%1qUksVEFbC3`Zz;v*rC|gJ6g5y9$rqp(JAwis1<5g%V)t26H5iP~;>z z+h{?}dz{l^OUmIugOGJO1+n1i_u`|Nn6vBC?;Kl<7GA(PwkQ-Hj#sih&{-lFj#t(X0TvX=u;JSUBSNLpqAEknejDLx zh1TnJ>};-506VLcEf^*4(0TerWJ$nokbu=BV9%GRLJs;0vwY|RQIOpY*a~gZc{Xfo z+vI5LJA(nYMSlz&?1$nia{_;h$1Rtmk32uXg^(G5F6;ZNXq0Nx|0Siqi&da>;PYGklTQL2)VOM5&KCP)dS08gP>2LHR`>mkNU*QcQHc^fP-y{-g+-+0JH9 zhIulPf!#_rLyC#bX0xC@&k01P7z^pEBFdQgdP&O@Yfm`{J{u8aR_rC%yXwyvS{wB_%Gjf4t@ButeN_(L`VXy>^L;t~0?aPJ_u2D8H^}=5=KWq&zr(2} zlf&&sL88r1Igi{Vh%(R50CKY+rdh(t?-oJa<|mx_Q;B1j#WUXFk4N5I_94YY{q;OBf6eh@t<`FmWH>HFCc+9~VO4aE3%E z07?3oJh?bLzLw!qe+D^$6u$3ZmpwXGm;oU34Q1$-e9Fkm`Uha(q=Vp6H)Am<{CVU{ z+L1LlQLy>8r%Q!^h!9_Ru!W=|>0|s#LWIp%AbPPmjiL0ppt=-$J&Erz*fBzrB9VQw z2y~#Ttep~T&w!A`Hxhbf3Mn8XoVnkw#c9bakQDRys({U5>myn3rwDs;4_mC4i)^k6 zLt+O%6oM{5riG}eD5D=#_xb2lb=|4LPyfh-s zHwbKvA}#hgK70(UteY?-T*>XQ=%?-{<$wkLO|XzMpgMSwH7<)_bn+f7htLIP53#Uq8H3LPz_{ zvSYgZeDH+ri^E;XN&kID!j)wFKhI2ur{aCnF)lJ&ZslP?b)gTpiQgursJ_C`zvXsf zui`~tEx#egv1Djli@a@RtH#N|cS~ugT(pQ5nUfaCy=*PNWrU8ZW|%T34X78<6kG0N zIGC%p0Pi3UscNiAwcJlUM)jiYwmd)_R;^jUJDK{33YP;vNE}t`;M(#KaZLGwz`K|R z+-3~bYxzC#JQbk)5r)&HhA`egP`*&zhJja$9B=Mc91yoW#&CMnex~P-#Jy?_Mt?1j z6ZffqFb%tjSF0T?lP8GRsb|o(Tb^Q=8`M9mfuH6mZKGOoYnFyPq^$- zPokh(o}+w^+J$jc%N~v-UvwUU4Qb(!8fS%^%c?-0CsF3;mCJjLmbdbjpwjtP{;Ay< z#va1Zcel6ouO<(4%C3Iv2eR8|*gizu`k@R~mCaCFKaydrZC{J5wSFuv1@@^>wAR0w z-!4Vv*o?3B6YJO}2h5M47>VO#=I;SqP_q8Jvz%YW+r7 zml<5bEPZcgoQOg;gI|Tf{w-r}95gD0&b0nu;>|7|H2MHF*7~EkZ83w?%hvx0!4WPFt5cO?OkSYIP7(6mNgvl zVLW|eGuU8x4&&)>S&TkmI8&KGt+(nCJ4V+BSrbL#_J$dJ52NJPVv9N2Z^~J|R=>Et zi^E_n|1x11c}LMXwU!I}$PC_61Xdvo_^DwtbAl`_L)34wsSeAv1WRkrwV>w@tyi9^SrEfq9x9uH+pt~(63Ds_U zjikqNo`ThE+fQ%3*68IcPdD1$W~x_P&N0x)ws-yjZ|f{)8uRmKl77qKJgx0rKc0zr zoSY~~Kff*#^Eg+cpxU?_W^{QRHjK7Us6|J39JY(L0}Ohh$6>Q*`}AxC(Cu;9D%w7y z%Sj%m7FF8D6+ffbDJi}*Sg8*9Z5aQVX zMl`K=3kld$XfE{vN67w1J;(!MmcvH&$LK9ucbY7Qiu<4iPU}NrJmu}|)?E^SKKar5 zhwVm;gbf(uvsy=5Z!Sf7ZGjH8j<&eSXxKlY z)?2HDtPXximuhL>h8a9Wr?KMXg*BsFSlli$gHO<2*2r4HI-EL>g4K!}Z1=2eusT^) z*lZ5P8HCn)i<_4)&-FB4jgkx)WNKXN1bGZ5xDbQ?)``M);uPv=u*t$;kY`K3#HT|>j#YMp^< zf|>o@D6lq*tF|z+bu@x8c@AcF+bpmd!hS@`*m2CVK1b{@x(Rk-?c$~^C^9$D<%2GC zmnj-IS-t`pv;K^YZYx*a>n8)>TKJFneZMkGQ-e?uC}Xby5t~f+a|O?;B!$G zZNFwB6fO`u#b{$2z<$O`RBhX}lsoF0@xYsj1L}I(pX=D*LTXzS`1(_kwy=@;Bx=!a z+ZJa8t56qhx6zZKc>iPDZ-~9>QfNWjc4B;ZX*(3e*LF7zuukedjM=~&v~9noiN-GV z?I^Ogdl{ru!sP%`=-Ed4BWu>X*{}$&a+j?@P56k1P77xtg%ds&xW@tf8@EwW30J`} z^I>SSGvR}O0!uMW_>d-1Z}Cj{z3r?SgsSfeqfobcI`aL7=6s^SN9LV*c0 z<>aW$mw^u(N$*LRkbi-Z!Gugt4RFP7@vJ2Q1mJ{ zIz9;?#a5yDYlrz@V=(s)J_q;7&sRk0{(#>=$x!@|01Q6l`8g;l-+wRNh4d##{Raw< zoec-MUa)YnzFv@bP8S}>H{|y!&luq2iTBH$g@q^V&g7D{b>@6HEXl{SRtlO>*vl^o z;38xQ|Dhn@iv6 zJX7u;a@55+kjwo;QT1FDxSQ!o%Jm~Nk0I_-jc9)}kL4a;w_)w616h14!pu+%+4=_R zF)QD(SrdUPiKnZltAU3Q$K-Y||8QPmo>bppNb3(#!MgN?_oH~Vbhr8k+NpmuD|?6P zX82W1+b*>ggB1T*;@#>`FdP1wA@H+DoiQ7D9CN!@zOLb~KxFQ(@)W~_<%be8s(p=Y0&{U1-tE^rfJ~;*J6RRYFco> z4Gjm4X_|k)9ZEVw)4T)jH6uZ13blL(DUzAhI(#r!4!RkFYChRdl!96OifG$8@ImSm-nkbNd}EOS0wqV{HFB7Uy92qb2&83%Hnc_XYz(&k5k;%iLaP1In{k4#h7M7V`wZq1)vq z)hp=<-WdQ}Z;CbW1OuqE_^*U@dx8t;bA!nS)8iR>`l(Be_(oxUp5Xd8+^!O~&J)}S z#fV=mY_lhLGju$@N!WIT33ZHLBW#x^IGABxD{PNvC@)5iUuUx2@Am|K^m)CxWwl}S zdxG6ir}z!R4tRq1jt1Lees&33zb9DCyx(XpMQ#syf*JK-HQ(E9Hg`mfU;Muf~ z5(ws2Lhz@wz=dE8EAGX#z=5C$Z7TjsT2KtZ+9(9CrUgY1oWpW@JuN7N;3F8z_#0_K z0R-1kQ{GGq@*y~if_-U09t0El$XisW;Er+o~7;FSgl{+bqeAsCDn8UG+H z@IY`XtKs9cz=Gf)T66qwX@LpBv((ei(gFj5G3Wr}pQi;C8@yF{2?)MO3(6r_T?4^C z(tVpA z)F88%J}`@jn`uE21T@HTy_f7Y3n4hl2Z1N;qX2?$DDb8Q`4GHI-PfDUUNaAZ7B-ZO zw2xc}&Z42qObdJvbkPi|v|umo zg3ah<<2h-;KnQqed3;b>kOhHF9UGh$Cgb{zJue(}FSx7TFM#qy>Hm%GoMQ z(}Ge6A}n=(T2KPPVz$b%w7`Yni#7-<(gJ6bH^^82<3WoSf3r9E1#QwWVOzbyu`G$< z!nS*Z&D8l3*8L}8u;LBw%>Wx|(FX3t(%S^EQNs3ngGYtIMholr20vj%j}dmj8=Msa z8z=0bH+Tk2eY{TCA#d;&^wsh4!i)jI=R3jbg{c8SzUdHe5at^Yyp1tLggFC(=M4jE z6c!i|9L_e;BrG%_*ntGbn}vl31T*G?O%N6t5X@%YCkl%W2!2N^J4skE;Hgpbu*l-C(We-w<}h3~~^WXfrc@g?`=)p5p_XA)|TAdf>OQqpH(>~K4*Nuop2g|Wjb^!kZTVHi7{ zh+HS;3+wU(PsbP_vA{GpVWi>-UJX4=9ATD%^>~8EqxVi6DR1-jdV=R8Ux}m4dXz$+ zC%AGtShuj%p5TMfoy0N1)?w^04(wQA8!!f7S{9pKYmo;}aKJHO$C)*VVXG&&ECP0d znFQPJ2|mx*d(0ETc6j8-Nn)9dcy@V2{}ap2UbyYnG4Q#6Z9(4D{Q|W@gzV@F8*Z!87PZ3Xts2b&J}WIKhoC+R!P#j+na^9z zuT&+@Nele&QAe#gPYXOQ1mCdA)}(zn5YPxGE=UWCA$XU?b75Lg1i?tAacx>q2*Elu zo5aOwK>-9bkBLjtf_$GBC-$hudH;gq7B>V?cUJq9mh+G+e%G&ks-EU%9|1g#H&^df zPr_`qPv>Cykes+{Z{e)_M~9=g_EuusbiP6bw8uU#z{tvURzgqOXK*kYGM!l|keMW5 z(;0>)&_0VKf-NT)nD*H+W;LCuDAD#fNz8O6Oaqbof|I6Gj6SkmKC0MlIvFsT?Qh-o|e&{j;=OlLqdNcVq0_L|OJ z7^$?Mz;QP2zdix2v3&_izv;|~g7lCaKs+e__N62TO=l>SpnchMXatAMvAiw5eYx(H zE4r+){9JN7k1?W`E(gc^okn}F=4Iep(ZjU!GAOwF!TCL+_ER-41?Tr-+E3HG1iW<+ z_)5)P@L(nQ>6$y>*Rwp&(7YJ@1&ln~&(yq#?u>7h=7r$%S$t<{UeJZNyE|a|XXhuO zO_@0uVXbSwF?2qr?Wgq^?Kkz^f^@3y`N+-ZYyyqh0IMMz)%6rry;`@nl!!j#&=LZ2;EcNR#zz@;?YQs7KiZQ!z z9I};}0(53K9|tVARL`D3KlwcZ^3J_CvwU90=pre3bks65%jEC4G!sx?pxGqvXIX_*1lA$B zlWH1PS7P*@eCTOB8&Lz%9Fq@o&J|TO;mKXZF}bfg`3Uc;PO2yjcJfiqN4wQ_Xk+q^ z46{c*n~~hjscElEbxb~~tGlAl<*Xwq+tAR?K6el4)7rJH&%GDPNXlk3bhFQWdlu-k z+O@RLErcc}WlP#?mh`#nSps{stJ~+g(6b~LMWEMCAExuK_i|FtkDpjuVQ6NTJb=Q& z3%>Z3uWG#o;9~*m9}Zd4pVm4D-Nc++es2ulnSuwuZ%#C84N7-0k~?RLt}uSeagKZ^ z2@A6cl*gQD2k19Er*#4S{uvcLr!87;0CUzjC&uTL;(T(>40^$R)yUDDnUp(nsD92Y zS$9ITjLtdQMaXvBb!X_>thT^QN04eN+ z|FX?y{C8XrP?P6jd~!cOsAH(7+kkg&Tg}>*^^6CJqw3{W$R8q(sfH@xhl!KQQwzL{ zxJ#V}jfnrAxLfffb@4}td(^GSZu}3#z3N*OK>ShSKJ^Sbl=x%BtL4?n_#cVasR2yG zZsHAUAG9g{1o387iH;-w6!BKImwuim-mch}#h)SGp>k2E@jb-5iq2Si3?`z)yHzR* z`HRGR6x(9_W#YYR35q%XD!Zrs&h1Rko0J>)I6(u*dnC$qMmr$yllV-h2r3ZoCvkLV z6aOnoz;s3+nD|Gmt&r*Pjm!AQBkahM|j6PGy276c(L*bOkQ@sK$iBns=(hEl;B66l1o zKV@-`G?}>U(e=P%O#5PVPDQ{@>XV~@)l8^;>eDB$H-ML5#Gm>+_XfZc4rfzZ&&>*- zd--rUrPaLL=f=_fq|#bm<}*e`xoaW_x|Hwc@8H#2% z`#m2Vqb9SMy+8p^Gx?3H?7?jREoS~D*qHhe%H+h`WF5}_X&xM6<_~y4zcdn`A=4XN z1ml6rO!38U2xa2Y>_0a_mN1V1#`#|S%D&Gk0Nq3VfKYr~u{1{c;&aR!;h&0 z8O&|D@ZD|x$Y3r5_Qj7ixuB|pS*(N6c{@%VxN&@tPtM1vFy13`G`vBJMAUL`zb-}TdKQwRZPCPB&FPk@w)(78f8jh26(<$~T z2R+5S78aSKwm}`|wGsyq2(6YiwiKeav*8s7Uh!*f?=*~#qJ4mwR{^f>7`pdT1j&xE zV;I-@_3lx}h+lS(IzoE)XavfzV*k zb~G)A{~pS!iDNp_MtCUW=Qeu#WWJ42EC)LUD)QeX;y~hUI~Y z>u~;zl;MC2ex;KB1>ln(CJioJn6O+_e@96M5{37F@te;){Nhc%VvEsiCvnPnKAxr-q!z7^0hJKYDaKn-vuqq9WU#t?+pSt>g) z>U^ZTvy1~&A3r?VSx)GvcTtg@6_;UbhMSqub~`I656PDnJA=f!GCGG6N95y_ox_Nu zic1rnV;E~py)y<_zGRBKv~z&Rz6W`in$r#(W~p?${4{oFdOxkA$K}+%GrgZy-s7qO zXtVBM%X(Z6s5=jDul2Z{rJ#qm*LvJ98$b_lui;r3yw1bhYd!7(D0%1M?X@0v85&mS z6diJLkDFpiOx3ig$9;hXc6fWOM?RC;IbFLJ^vEYLJ6mZ{dgT+Coo)OCW}kdAt24$= zV6Icm6A{8p&KowUr7(`2v)QR^mN`l%_QNUm`eQk8lKyw7aRY$ovH|Q; z56lKm5${%?j{=@YKYL`Z(Amkk!d_9_&iPc_{c0Fe(z$?j(XaNv$aWsVxy1oh5(7Sx z^Ob{&TP&T6C_m(I`>OLOj;9RMVK3R)%?=aiQu!T-&STj$a4yw)6b-90|Bt9Y+<$>z zQE{%W`Q(QI!-WcIjn0PGLVo1aA4xQ~u19Db_axFYw%e+KCo)G0XBB}b5&P5?Gk_-( zJ1UocqAZz!>cW^KF@-p!WQlJYO$J^<{0k~1G5u4d!Rl!Pj4wn~nX>?U5@-IE(TexJ zqke_7$#_4pier37_Ky>1a{?BS7j6^h((;64XrDNbMiKkgsIba{sq#219bS6(&9p*J|95@mpO2;iW}f{ zPBA1n%Z^|1T0^f}R^vy!`kh1vbD^#t0neSxn<3Y=CFa`{iwR70u|;75W1-0=7BC1r zciA=c!0$69E~oj^mOpXDGcZO<(XJ)r^Fny;C|HKXhNbXd)^8@RzI`Kn&~hX;Ng~uc z3E*pV#ZfI2*XoL+x+XU3iu*?)@O8uiQP;!`RI`wLj6AW0I4tUyxRE%bb|6WKn~9^+ z91>fJW6~TFw-6`Q04DNQ;x6?)Mnwr(uIN^e!x|)Rr@Tj+Vd4%p!(LUyTDg<8vRdt7 zdSvNho%)ISlWF}1^#uBx#69%0S;MmBn6RbN-tmd-po?;#XI4;Y;o+iP> zYGexJ8IrK+JcOZr;yJb-%(7mGEb%9%D{4BoVWyjSo|T0uS9dPR3nWR?sb}@RL~mWD zb0(j8nWWovu7OQSyh7501vMCw#H&m-rfI_mfxJf2XF7LQfxJ$#+H`(qLf;@+XF7Sz z>02ZlOlQkXko_c^G5g8~`SWrVf!8YEirE_z@fXeg;CCqS_vX;uECv7j1n~DYFWG92 zmPOwW?%pU1j6)gnhfL=V(~;HXk6CQHOlMaRE`OuTZqqq_9>^yodrar0ZjjH|{P*%C zK3h(P`T*r({e{{2@)iinVrJq8F8j;8F7czD*UcRP_y4fK^}H_eGee2mV)yj)O;79+ zdivHS_ev-9^sQTtMI`j}tw*L}2|Z)$m8n=l&)E9xI@Y9~v8}cjFoSx=w$8qr6`^Nr z8)ROVu+tMeP9PIG>4_a@hKa%Hi5(}937?Q1GBZr%3fUzy!$h8t-8PpL68S>**jye? z6bRWX)6qn+ko_{TOO(jOu0P1Re4U5)c|_Ed z-O@zm;fX{Plf7oD#SrsR)a$6O#4vLg3g6};Ok%i9Hw~L##7m4YX_I}n*yNB*Hyt|< zm6sSLBw**0j5hh9!H~@(O^GU#qtdWFF9)O=cdDZP}9A$kZT5bnWfmu;(oX#Ma|!(nG`%KYLCmre*W6g%Mz#raf*3Iw3g zE73=%Ca;HMMJHf5^|I;tHOh6|7|35SSKfeN?PJgYQ+tIdxi&uast})@2g{Lq-Mk1h zRlHwU0`i8CfW4>^WS_}T=Y`~&y3|`jMoPJ+_M83iJ<70V!APf~gAnJCTEMMVYP|W} zb%>O+f>eFFX1N%cYLuy|ujWsv^VEdHy5iJSRe?`DtSU~;$J4+M&q>Ring{6~O}qP@ zn)dPFQ_{TDsga~kOY;(^rhFLOTQs+@L-EQa@YXaha_nOC4XGJY0s;F|bVR9H=6NWA zkQ`A;waXEuu-!NaE_39-Q^cM;A7rk$L}k*MnkUC)VsgDqYQ8?pVlQlfw=T(am(6|o z)B+(r_9k>-sUv0EvCn=S%`df3$ZER_-C61=A?xh*Xw0djO?E6B>=#-Z=FRjE^i zVL|A%dazT4VL|AlPO#I2VL|9#*#6Y%CO=9FoASXFurt#(CHM&jlBreF=&^76XU2Y( zu$UP<0yZLbwlM75@|;EL9ARB%kPEn}bA@%o=Nz!}g!P!gPe*{AFRa%Lu17Ja)|l)z z`f%=?#c-jp)n?FP)3`|3I&&xw2c^~u+klsunA?kmZ8n3KVv3NuMA%j{cr4jEVcT)( zJm$cuONH&g?Z?dRWx{sh^(E^0<-&I3<)RL-D}?PagV``tsrAD4;&48iI=1YX&i!U^ z7us3sN@4wGu!pt3LD&H^xGV&=QP@E<_y)!*sjDQnhj7t5Y-H+cal=_SE)J$P3Bv^d zJX4dpRv2D2DrH)(ljm>&z&M!Y)b+vwR`7b}{YFt&TmbNl0@LTgZ~?&aY%<$TcF?#0 z;B=Sh+(9^%z8tfOXQ%hK(uQ0J2&;!tHXDHfxE1z1omxk9LNbTdwqin#W_Mf;0 zLCLyw>KzfwMoa3?%njGIE`wKVA8Ya5UjzEXsrL<3k-DoHqW-JUKn&H-hWLRDP<5yu z(hFa%-ovMBXm3Ay9S|1+!eLY>HQMX}u(=488Y3ED*j$B5RS8kLcc>QA=Bt^*#D}$k z9#|Bp$-&w~swQpF3j;>>umup*n#Bk+LtU_mzYO1cGlk+Fe>q^Wi80k`GkMDIw;;h> zwMw3<*GYJSFnJme5GvJ!fRm>)BR;($cqS{%(Hnw&!~yjy=y!4zaY+3eW;S^?aag|E znmm^qe-YWZOrBqLGb=tHttz?Z_rU!Q52YqAq)vXc*mp3E1C3Ob8I&%FS${aL^*xt z*e_WQS81gw2A5sNt2HkIKL9I~+@yIS#wS)SOkJ}6X_QrF1z=BdbL>VaJ??->UdK2L z#qGW1_1s5QsvDBz4SG+tAO^gJiNc;Ly7=Uc#32<%?Iv#`j;PzvNR#Q$cJoWg$*l}C zsid{v!nZcM6pwNyxA7!rw_%0&Ktl>i$TR^X$(No5mXB8_UuMx5Y8|WP6|RRU*{@FS zCHBd2^5m@7RD2WC!4Xb!G8>Yfj3@PnqH$ORhA27ZTZG`>oSaTI z!8fo`v@N=5dCnr)%A_fI9Xi=Y>~oJE0l7ZytsKX_fR(I|cbmnI+s~(FrR&sj?__zW z&vO?#?q!(HCEK-Yf#XW;B{j`=)cPn~=P=O$y-_`vF-Fy+C6K2$#*3-{GKUftL_FvS63&A;#^3 z7`P=*WIQ2RQBE!+4y*G5ke|e~M~Zq+KFvtVN1>yryax~uZW~1ctly1*rxg(qQ1des zwHuq`Xc00zr$XuK7xlx;00Mts`-s16MFM%qpgnX1nxy`Kb^AzZ1p1`UXnKjbJb~O^ zEmcrpN8N=;R{L0?1Ffhl=Vrp17EO2MOihiZ_HTfPwAzL|C0qrMvlKVp!24XX!A*?zqjap8hD z`Q@CwHj#_bX#Tdtohi8lL5y2={3`h766ikAx)xNbrsEL_qi$i;n?M?zbZ}qm*fRcU zb_7==`Hk%!!cB7_$;L>Ez-cVO0o?eM^BVh!sumSGUUUvPCWe733!=Ew?W-J zy!O?ws!&v(E0z-0Fq-v*hKo8^Bpy*?P!R*pq7OqIH5B;l zuL+qXgTbo-~*=7i&Cu<|0$uXJY-mDIiTp=s7hB2Oe^M1JWWxa&_W)_<$ zGGletN_ulll(um}RxiD|W;J9NX6=V&W|o z3)!0W6G@Gb?O9$1T`OcqRyN5vb2Q@KmDMm6WW1zpch)98(_l`Aho_B!kI>%@nP)-h zWZuF|syvFe5gMw3o^NG#mCBk9e8;g6bBz5JTyR$>!q29@FxmDVxKYaL6zROfQr_Kq z1V|43W~k!<2Ms<2zdq&)k`sV47s9q|P}LDOyh~E}3HQ=f1*YL={6<1`1_qF7;vK*~ zb;exaN$B374X68oqh|yA)iDjgQ!WM$s1q63G^To}8U#I7EetcHRtyH7$r=i)E1;|@ z@h)&g71jYKKLL)aTVVXuk@ZM&Ox=z8Qwxc^)LUqyY7uddx(kL?oya8j;l?&R76X)$ zz%j@rD2S|a(pJM6fM>CtSd-e(0kx;#&ze3SC6%`lFtZUbX*a$OoO_&MH+>3yFmjh4 z(`VSttj2)6%4tvd5b}^33`MjjeghmE=POo(P7E*YBUNSHYN){mh}cEKR{$=nB+98`NXuv&nu z4;ptAg3;%M2h~*~E_?|EmT*x0X^6|G7Ngh(;p48X7LJw%HN6OVKsJH~HM7D)if7vg zP5c(}u$n##coO9i)r~3SpjNiysHA65ES>(pqx`cAZ(@4>0vH;+;2oy_gyW3CN9;f_ zdgwa%$lHOH%7^w0UbGn5awOWpM=|(-VMS^YO!3DIrV4Pc&$k#4Fc@F%FTwcobTBv1 zMSk-C03VJTg}U?=79v;M)z>+|MZ`O#lzk3Uxht0|D!w7SX=Zocb$oVME271ucTXNy zw|v7jE#G5U7h_17?XJLM`DXxH`=I-|$5KBEe0jc`Ab1^X3i z#w5eY%RdhytFIKn_^9O8R`hlGxo4s|!H^@HRtK!vpsKgvEkpeUzl98h1@B{;B_^N5 zVU>sAv)V*`Vx?I>M$}tXh}IkkZbBtEbny8!E!>H zmdEnu%m5v&X<01qffCRvP5rUFf#?Qvsx>W*<*`NNjMcOxmiKTQXjoG>mN#+)XpN>$ zEU%1dsMWMMmUjhdou);xJnqEjjMua=+#Nojhdu#qPw{)Vo zT&`D`gIMFwZVS-fve5(Ay^b_2us4(YQXW(~^bsC;8KOERbaq943SW6$LQ59A)?qVioLplJo zANgl)X@Gy7^kOFc9Yo_wT8o+1;fPr_EKHZ>d@5+^MGUQ!q5T0=a2Ec!5_afKBdx8H(U*UOBpdRTHW+>i~g^7ub^owD$JWq+2PBd zpPwS}4&gA?x}prQ_6|ZlD|nsoM?B_To{uoo6NHZ`)e{8CPy_16`}A^3FL3~$VQhz| z4ZLG2J#FA~ho=qY6l1^|LA7U=e}C9hWqlN5qURu4KN0IWNY;Od^&BMY zXJTB~iHEWb`3zpjhVeDBOd(Yq-g(-C%PLf)f1!WV6jVNLEtQsNPZO%cmYK82uIS0uaCmU_MY|cTl#!EtX+nj@BHJAet&(lT@=O9_>IY`zm z%%sdgvbGWGIY`#6%&wk;WZl8sa@_DRWKd!TMonh2rLrkyb&8aiV2YG;kO0Ua`o(e! z0>Q^YAdqV?TLAReporiy3x9I+a9IofW++y1J`V;T-X!NcFnYo=uzEE5JejZz44;i> z^@L^M_$`DoVHr5_FFU_D_O7`+%+Pgn*{*$AvBECZ)8b$Y@wu!Ujj3CqBl ztWG^)8JJ+tpeHN?lM3n66PAHT-UqBFECUx3>j}%iMZ|i-GVnwuxzEUA>uLs+0>Xsl zz5+B3Kh-3YhY)aE*fQqr!s#%-o9WR|qc9t=ucO$~1Y6qGpy;dyfc3|uW44Dpc9AjXqlLZ@@i_!CHhFXxSEa4%%n zm$OgcV`xk{`%{$Okpg_1V-_QeNvBZqg>O*tak>c4)T>XN>8mjewYsEr&hgLDuU%hq)Ky*TF+y04=-)&13NJOsI0v>3zfhjK42Z1 zjVOvvptQ3Ske})@to5i3Uy1Y_A0`3IP%DP@43ys|#xk>DJQO3hokLvJIRh#^#{x9e zEiUlfFA%a)Q-g=Gt@s6!+|Iv3?o*p#gmUNa1I|+yGy!+fe}USE`pP|m;W&y5qPa&B z2h>!gAa~(+@Dox=hIusQIFE}?BllQ78&Ufx?;)Omy)FkqADaVDRu)P>KS6sf^W*&X z_fh0lHU{4LNs&H;N-G%rSNP4m6L3%Ad!-n~D64rdpBLH{vT ztRVlg5`!Du`HI{W9;^rMQkxopzb5Wh8AE{oN!+7cj8h7~X#?(6Gm!1Ve-Zbo2T(SJ z-x04?@1ut;{GJ(Kr#>hCH}MAb9q|vuo0SJDU-%>ORyCOTC*tj@l=wfyJJbl`pNV(b z72>G-k+Gt)hyyAU?X~FazoURca@e=%925R^Ar_sd3(-3ge$HpXMb&0JRg^xj z%GH6Q3qHWJNi}mg@P$l67gj6-NN4WLNL%rGz+S6(_;L*V6kil69&s94g&_$l4lxl* zA}$`qh<$3`RQMUqOP3t=A2gccDn{HRZ;BR=U5jUX)tf^h4|D6XPu(yKxQ2dKtKr82 zkGmXx)~QPvP95bN)M|uWJf3*7s_Fo4_zl|6Rgo|p{)?N5 z`<)+AK*bY@mFb*=axI?p0wTr+>X>Gb$;X3WoAfCtb@3EV;(ASIg%4yZ$#&D($^=el zB>krIA)0ovY`PsVop+dxHfH0X>Exh*ieom&A=7yv1u}!gz-^;U{VY}^ZYX7a6~|A6 zp~t19wIe_hBmv9WPy^CVf=fw%O_C&uTF!}0Aah7?+h``sbS_CZdJL{$73B3HZoPu# zG`j-tl&zR=@qPkf;{-qD_AA9*8?S)re47+ zsk@&ABP&=X_1wGBD_A8F?knjPtdgch@S|6-N}8$DdIhUw;w6yl6|9m;TGzRPRnkg5 z)+<;gv2^;mf>ltsjp>mUtfB?Hc#c<@6&=x!(o8qQBU!R~1*>S$W;6}Gf>m@BgAW*1 zPc?!m`jo+pNA*XF;(SwIH({wsywrk%ENW*KeYo#%rBRf;K*Fi-hI|g?*h{v6=Msn1 zXY`+9_}Dq-3Ryw%41}py$Vz>DDV@nI&HW{rrFlA;Tp=sX=gyp7AuBE9j+b5`D=i}4 zA+=cQuoQL`afPgO$f;-ryIro3l@4Ry#T7D_D`cg^HRTG~tq8Qhd5sCV7O>l2e(Ydm zlyB7dE0|cry?8coWjX}q&h>%@(;+DLv3AgOfMv>Uo&q{XyZV*;R5PgF%QQ=syNOiq zR+=Ts9l~eR+mo(xXQBc4>-DpaDmnh-6aIVeTCtLy&cpYv6)E=^CL{e~NTG7OXMv_) z3@K3VBdD~)7p>(heZyJ$)sH;ojz$&_-<_7L+>_8-`df6oKIQWCguhMG!OFcCMm>F5 znmGt`0%&?SJx94VD)R7UX|{6rq5S>J^t0JYR_6RG4(3taxwGz>s~v|09L#RqY$$m!l8sLGlRXp@8 zQI(ygqvC5iWoOgYDMgJaJBL`JDmyPCs`(H~s%-UrD;U*G)JNGG&&iDH3iRS-7rYI5 zSPewklwCLo@`%{-vbCq2!bbcREKb=aROOhIPT8d_ouu?XWtY>KbxDgWTfZDSiC4JB z!T*&U75B)6gk>9ecDGk9AuQX-X49uewZqTV#Ou^9j9kk$ap1bSh{wdsu00d+ZdKPX zoa-6Rc9lS3l-@H37``sF(ug#uV&}o(3r|IB+xg@Y`ho*!2U7kHGyI<3ses>$JVc7$k+Wjt17?$l6Y7OjndHk^K zK}}V^I~0mo_OSMx*>6~P!nwfz_gpB&&45mMBk%c>aa(!Q`$rZEYREDF^<&?_?xKFy8@)nl7qr;Q~ zF9FeYtjD6{L+a~!cy=a(3@d)SvwRkD#0?^@^7tIqf>{xDU&pYuJfUfM)a7B5@^($j zqVBm+iSneT{-|5p3_3^C(y04a20K^Nk|=JFtwoM1en2!C%0wdaMg67k&robeI0^|L zfik$^4NUsrkU{R+gn&)gx(FGskh`*FCn9hxl*zDWq0Ry)cEEEf^~!n~$*h#u0IlOt z3Y7)?j$CFRMt7mgqGy2Rv`A&KK1IUw+LaE)O7XaMrAzFSqgRzB#E$wFS*tANX&v3W zR{Du6k*wq4Zk>RpT^ZO1n7IJZsT`IGjZ{36TsfQ}=!;J)M-VHu7KKt7q8C(j{Sc5L zc_10e!LQsG>Z8YBehlk%DD4m*6KoB{2rQVp9D(|MLw(2Lks@xv49%tHXp=KEk791Y zFqyIiGcJh;T z{0?To90mFKw3J%<#tm*Kl~i9PJ~!hxpW`g;i-G@-SA4Vy36||7r<{u{vYq4vUd97@ zJINWs0iiTBC%6kxSBEq72SOS5Im76M%R*1#uWTndBYm*WdOOJ(&Eci?IfhVgCpq#8 zC@XXTWd49tBNK|BVV|8^DKlK%j=}6WsRsRc9lhx7BuC~1+)m>3M7EQh`er!e)Ex!f zPI6_*NVb#QJbpw$Zzs70>=5NRms_Z7hq1avIwo!>xy9^t^mdZ#5bNzEw}gHIk}|iH zI3(|Px_;j4rMHvZG6t`=liYIJtfsT*^-o#dtmQ%=m~c9NSu`c)irxt-*u&w3Tb zTy7`1>9byiu_A6KxeaWE+)m=y7H>NtTYjHAh4-Rq4{}M1)@OH`=rfE(6#|;hZ!%g< zu;y;tAP0pV1nhMaeC1Qm71~)CI2XFYaFd*VC^>59&LQ^6`@-&AVn@0xcb?8wML-6q zZilAj0rxK>5l5${WdZlHxuEkk^#|PN%0Rm`Ee*(9y6yr^O9CpK3)h7sk*1I;nGf8< z+6~K?!ab4t6_JC1?lK-&ipnd5?n%skOxN^s)^wM=x$K_IymhN(qY+Lo8w7?;AHnvz zGX}#UL-7WCIRMD`L>Ya|pJAEP$Z+ob@d=@T?>rvl1Asj_~~sFmo!PZ$#cc zhW6@Ii1P_`V;xZ-Vg$fZjz1Lg$xQkIgcizWW^qc7IFB|(iGd5r;Wso;XmF^2a!1k? zDrC9>hSiBNL#XIoglGK>$w*%Vd}jUw&>5N61FRVQ$oyY|A1T3TLo>3FUTA1|pQJxB zQksgC$jA~pA>lQ6yp+y5MI-%G(SW3AWSN*&Nzur1rU;X+|DXg$R;*xnzEQr<`5+I^ zjnX-kZfI2Ae1xIR_NaV%8K}=Q{y_L1OTw^vr{ST3Kk9IbN0j1$0b?O@2z7HA7cwX>Rgiw~R)Mxd|E!#Jtx zLJ?ew-@#RQXfuGcIm1$Dlcm*kx6YXYIF?!>r>ZbJQ) zj;9w#-3}97+R%p(!fGGPPicg~N0c!FxbaqCmU$xvqlW+>upCT2A53zzgFMl|X%#XI4y_*TdmhY*L%;A@Qg z2*r5tL-D)@%(}Y`4kcFuW~klx_4}%PU(?&$D9@_gnHcC9>Ka)4s(fNBbtO?gRfVt8 zZ?wvp3N6)sO9<0`{UTPLJa`oo#I>R2MpZfY9~^ZeG@xoIYmE6~xYi;JpsR*);Avfm zr>cENA+*fX0G;Z*D;dc~6iszL*Os-ht}YO<81d>t%4sH3@ahj#yD#&RJCW<^62@bw z5?&4 zb(pwYy@2LdT^qx*J?aP4WOW_o+tn1tHJ&qq9qu*I{OSf)IxbY+A+52xk*#r;48*IY zW$s3)ur#c-N%(Gl8_*vZJM%-NM3xW6&f>J+kmZB1vpMb8c7Lp1x5dPx9-m`KJOuyH z9Q<0(!+){+GhpT)0F%XKu}VBeV^Cbq=z00#a-+C{AC|z!wx>cKpqGe@CyFcSC#p7L zy{UKzOFo8`K$wH#(iA-56Rr4_F~o8JpZrLuxXTA|W~f!->wNj8Lx$pbAVX~w$vy;V zaugT6!#B~4 z2UJcRIQ>@6os)oX;nI3iJwF6^8>f&x>I|%Ig>R#LgPMz{!naetUAGd zwSfKMr$(V5WkwTzx)sn+TtEyzBb6ergM^=b8`<~i(*Kh#eO@9Ret|e3T~_!-8sv~t zh$H+GaYRX4USV2dDlZrEy-Z3{?>mJ5%wBi58VuL)U-;}EbvEOD zk9e=vw)d%R`wi>y$#7`mGJ1w;#&15$!6$`*@1CHj{GsqfrcqjDc+xMeGCcX0RvC^m z+dh?=i_oTUlon8YMJzm(c!*)0$h1$Jjd1)YoS~nK(XUTGH-k7&ZgUCG zBrY(lvMC5-))s~l2`6R@I~<1W*QCQp{!bWl{wIvN5{4{~h37GMj=b#`?$FgJRWzT) z6Vg@G^<@B&8P;d89^nQ3c%T4BvH4-6LSEJqK80KBg;JZRiu9XyNKd0NER=d$c_sX! zOzAH}Hron(;d6H~fK>zG^*rvK7yh2gd_JY{M2UY_c;U;aVXb_N*fjLJNa3T{7(%+# zj$u3zB>^4J$D-;Uwvis@GO02UKzJ$fT=g9r#WMQuQun}QhL;m}7oBc|PoIZ)d!$1S zpGnh=t6r%leSGp%t$M47SE{=i`dP$h6rEmggjfF>kM^luO#THFualRO!fUDV8?^Rc zOuV^h<;s(c@FhH;u(f36;$9RR z-e4N{6|q;km~mfo^rPgyqH%{-LXLUDlX$px9>cNDK~%LJ0w_w>6+;5R6g+M@)n9{_l?z;`entL+xJ ztOM{Efq(1-JXYY&QozN`iS?T%z~cn|8%0*TB#v%9v!)G-H}18qz(uF6Fvh*kBLWzq zABOlm($eFMac}TBpDv^~i3@bW?Bjsc(FOAsO5NZ>f5 zuE{J#4Hmu2I)&GPf#_FzVU_ z1_B9qi3yGbR)meZnPw%@nbZl0%d_2>+s!}(=U&7Jo|wjrEi!j8f-{ak)u=m4-hDO# z$DE1`9IeOWfyKufb;p}E2n!0+*oeo^e+o}zhN+d~;$64xI&4)okwu_kSV-i}}J`PG%Kht~* zE)jbgYNdX*kf>cb2qa;0I1{t)M9Z#EN=YQK5-|ojxZsIV$ib)ZwY~mnb0aK_{WIfy z#^k%;hMign@~k;}1`5Fb2zp+>N8gO2dFdHO{qv@cXPf}>3xY#(p=kY! zrXLQdNEnRzm*kNyotT#;G2OasUJ;ibT^xI*IC|}qU~uYR6>oj^5{wt>UlVVu?GI38 z^{)%L$bJYlTK|T`v(B)lqNr;x+=AR>QtSKbZ(D@S%BETU?Q9f=nt&!*{~KbjT91ZU zFNgfJMXBG8NYS{YD(*ZUd3V$ujP0(AfCEY*yZa{KkopKYtH0+d;IO<4Q~z7~kEjzc z#IC>ZE8wVlhUNY{p74#S+s6U#LDig(g4E2 zkc9ej8P&y6Z^7KwU-wLmMb^^*d_Db#)Fn*$4Kv{a4NA7YC7^D2h5Y#i+gV zHKu_JBI_bIT?=O^>&UHqR+n|;HkPGQ(@+8tX-z(qd*nCFxq}jm+(8sT4MesRg{0IY zcM^xC)FXEhN7Pu1StEB7M~hB1BKHu*q%I=AWqe7gi^zSfi!LeC$PUW8rA#BgW103y znMUs4h|=kkGL7tHnXWdhIGS4RhHc10=IsGI6}f0KdU7e#$XZ>d-B8ZR#cPp&r5YCi zUveL?Pc@^pM=pIG*in}c1-{&e@B>oTkt?SGhor0{8@qtRQr3~Hh$A}vS2LYa#j=iU zVp+!&ck3e8Tn<0D02GESas&N$8I^Nc?pMu3+A@_NhPd&f88zv0Z@iS@=yGqoOqV-# zsPXbMr0%Ihjq9bk6|FEDf5j5SHjUAEB~b{cq8c|4g;7I|(lV&ijnXorDm4mEU#&}h zE3veUq&f)AX_S`HB_-W>ElXN!bt6v*L6d1@8m}W>ss2U(H?WraWC6c%3-M}|3p3n! zBk?*_0AtX&jU}-`y-a-T8suQJdV%;h;;n{tKAL3h)$@@1%(wFpOJheCIy0%U#!g*h zJm%6ke+j}=>R!}R;{sxzIszrycqFl-o`Q94Tttj3TVXXCySD&`r1lz*-3uI++G{-i zW8jF?UgHwtsLt0?<|`((*SL&nPO4*J&l^{kBb{C93Djz1-)!J+6@x`-T+<8OV_4-- zmD=kIkmic@b%wF0X^=S^N@#zFd^HU=dy!Yez9AFDXI_Fwlzb4dDc2kVJ@na}#x~_i zkLK94uub_w{BqMvQ-K`L57;y)O@$`wBxF}3{-$D+su8j0F&;-q)Lu$&u5<%2n|otT zB|?&RO94o!kS@C_0OA+YZ71ncW`>ZQ9(!#rNV$0iQr&CM34>Ia=YaIt;|75Q%o{;g z+gngFO_k=OAnO7v78^}N%qM|2*kjR)HVrjD1leqVhHk8Bn3;=oY_*Hf*))aBVvy~D zV@~fgnnsx;Kz19}OpeKJn2%D+{Bk-9rRmCZfYmFEWP=asGSpSsz#CshC#JBGfV!^T zatZuYd=J>y^t9Pkhl*c(>Pbe^v*zarHC^P-{cn-)Nf)`4vL3}aGVo}6K3(LJ`xhnm zfxyz^dX1)+%&!ny$bO;<@$11#*!}_SrfIJ{Q)h2rj$bp2XjSaXNnRHc)j57savZZ4 z&V|cc=8p)i+rBXhvfs2I>#@Jj1$kR~YjpiDVsWbWMvmUguZ5|w)*!^%n@C0 z)bzlkz;Xdd(@q-_LoKxtfD9>=>Vh^lJ)8%*PhDIC{3tsu%ne{3n;xTz2XL7qs8OTvvm`7)KHr+S8BSMDypPUZ`%5Iq{e4hTt=+D#e;q?^E-4%8=4<^ z7g!C;#~J@e89u&F0u$f-*w4tEQs<&wG(SEd#q>15d^GRQ26kk4)cn+N;DEZ)f&7^$ za7dju5BRy`fx~J-6!=dU0pm`|O5hjhKdS!K3H;KXz%jL;5crj+fbrE8D1Y;-9{{7{ zKruGI_9Jk&dVVDE>wbjXBg3`kHyBQ@n7QUR*`)fUqiNp9^rK_`1_f8U?GxtlJrvBL z<}Ws5C?iW7&3_+)a12!p)6@J9`p0LZ5km9Vx1p^54`J^C9#yrqkDq;JGT9`DWI_rj zkTP&6p$9^V$qt~ax!?W%&p*$TGwWS@?Y{Qf_3RVWucF*)e;kUu+xj)e`%@9*aiur&{xkDl zp}Qim+FyQy-%7U}erI~B-E#Po=x5l zTOp>g6EB4!gb{b&h`7s6i(o7{Yu0pQ#69-{_r7N^;xofbGTudTSlgj>;iEG|W!(=! zFB9OmZVAEVxQw9-QMuNX3V@R`sxjaSTB|V`3bzWdt#!z9_*4NQ>yM!T=Slz-R$~xg zoq)mCgz*69WxRYas#`8S4PPoQ)z)0Jjqp_h!qyD5$?!D-B9?>9hOZMa$>L&Yc(Z`0 z)eZJ8yhXrti-tLTV+LD$+&Tia65g6YwMtnFPzvFjM6Ai$PO+N>9B#db{xf__2CYJ~ zH44x0tr_rXw1iF^VT5nXxalx7htP>tM)>xOlaGLkgbp8Wgzv~0y_6dAB8)@$P6-Mc z!btAUIO|Z_dJ}dke6K{mIn;dQawB}7h;6f|`r-RCsQcTk*O<*mGHyp{v|IPE&>j`g zVa=!5lj3KGbx;?8r!rchC_AkJR>9L5G-11}(O`w2&G;F;$!@EKRq(usbz0FRzzZU_ z$Kt2l!!L>0UWW}d{6@xXB-Bt&0n2ljWLhidDg+dMH-nQ^OF8`Nd-%PKd8qnI<-7z1 z4gWLa#|3De%K2g@(8rR%h;shLD*7~otu?Bg|6xIXmN5}A#Fay34}UI_DdkK<+X;Uu zXaVjugi#28lMzO7HYtbi>W9C}n9zjUP|i=)KtE+H0cufw_<~6I*NhM6qe7JPZ6)0H zNhqt8GYCCqSOu0NLv708;2>-UXvx+qCj|=~_6Hh}md(f?izp*NHQ%P3p)m2`%mC^N zw*ouWESD~V+LiNA1Sm_w?7+>5)RS!Sw?jFLsj6KCy`-GqS%NtMs?AR2w6h`Q1(qO3 zyOgt?>C6vM4RnHwj=&W|)J; zUxn$c&IcMIZk4zJk)aF|H{8GYg#$E7@({tDj_6{-qeU`mI_nr_bzlS15;vVk3V>0iU%Uev8m<+8O{ViHHFa#@1h_Su&c)+_>cp+ZboPw|8W)hdHJ$EEbyQ+sZ90!q z-5UbX1*6S$mO#P6GXl>yp>9oQEW?Ziu0ZUYOy`neKy#!XHk;17tlN~Jt)?@c`gyRR zZKiYW5TM0UF1Mp3=Ar&?W?P?8PtWj;0|;3Os^VEbDqn>)W(L67 zzHiXBDy^PDfVBdut%1~w^L^CVh&2$y?(hcRa)79njVcdc?K>2q#jPJ$px5|L14vnG zncHi9*8?m!QY=X(fZo2(nr9fa@qQByZtHr4%6-xF}z zW<5lgC&XpDHH6LSDc>h>X}1nf06gpSp>1_o*R$ZB_w@$YA-z!eB_G@1PU|XIlkh7( zuG8!eEjxUa5q?!br?r%-wNtAT+}>D7$<#DB5e%@A=rI zgI**4NXpUn8u7=1LS7^OM9Ql|xrXmwJ{rDC<%seAOp2mfIbyxP@X>llaPc#0CH$3S zIqI3ZZ$vWg8O(2eZ0@r?gSp2?gPBrJbRPVD@1q%P^$g~}eKeS>l_Q4#XCDoJn{uRs z`%}VP@0k$4pJ8rP&ZE?fEI;S8o0M}J%Qe@(1u5GM!_X5b=w~%;Q_gM{SvNly9k;_w zpj!{;3+h0sX-o?Q?NmBJQvV*HPFyET6)Tf4_b6v9`u1=ye?b#> zGM#Q{cwtA}4Aa@h#^2vh1Ex*aTvzxfKr(2$=6Z;q=GrzL+k)gs$x+C3?qzL`@*fYk z3e#y~DUB9XX*$(3pA-F;!L8bKvM`zrPnP&12s0aKs{d)YMNOxfUCvB#i+iS^(f>T$ zQl`_$8avGYGfq^UhIJ-1tWK-?V~{i}A4ON! zMP_N3(L9ud|Ht2={|wZ2s19F!g??CnLh%@bjST)w0a3Z-tk3|Xz?svtE(-73*c%>U~{M_mpOI`L>@Twir2iq^1rm|cxX1Ya52RnFeudrvI?spu3q|&g*|3il8dr`?hpQ?~KYlbm7E%B-z-)mSJ_ZY`R{q^6sztGn zn{r}8LA#1kphutYgIOQI`Wt-#t8c&wCoVEZUnJbaNp9#ufy0}P(Hr@-!U0DtHb!63 z0~CVi_C-K*^9Xk<4uMvLq4ana0ImVu56>L#Ck8ZE&oUo^nF`f>$Spw8&YBO?+BqC7 z(QwZ2gM7dbH*W(vo_6Anr8v3Qr^rTqKp&lU=Dw`!EE=1phMHJV)#vtPbes)6(43{om4m`SY6X&uM9H>AFc{b-DgLnZ9re=op?k z+=a*cUMMap2`+pD5G5&|O0V*;+E^fw~umCo5|8J->x8F<{5W^ygv z^+*aX)U1GhEbY$Y1FrjjrTd?7!uwz7R&%(VA@`4PJ|LzJ7nz_18H3Zf+{iw$i(!m! zGhMylu@6c4B1jxijFSBQJcQ4UJG`3%DG@)P;@r0i3MG=4143p4esUOYK=X_X-jf1D zJaHfD08ipm9`qTcE2M4YA)vRU{cukx$l$?zJI&0W4yF{uUo|Z$77d{*q>J&B<0kvd zv`6k3{cn$d1>t$@10hK-_mm`V*NbG=GxP~K1@uZhbGTI)&{~qsy(aN`4ZZRKrv=cX zfW8nwoMg{fQY(4)gtn!ff22=&{8haFcS-#U$s9NE=hD8pku34LNtQD5lAHl2T%Ckx z4tMY~Z-*TSRJ>(r9e<>Rv;gcWY_`C8;nJg8@vYuDsgxr-=CJ@#;+Til*s?*<3KHXH|?1_ z#sAy$|6QC#kd!oWQ#hTyhBTAA#%?&vkdFhB+!wu7kVm+JUO6v>Bo5%>nZw9j?sdQEkmN-+xabuGQE4z~KU3%k2vKN77pE)2aD4cdT%5XEyjhF-%*( zRSf!Pdd2HdnV|QQUm>sD)ci6Zbfx|p4X5T;4mGNE4#o>LzmbmUi!qX}`F$^BqBxh2 zt<66;V2UGIA4x8m1M}Y~#+pADfh<22I$Kc_P$`TjEW0oGnL^_hP==%hX!+VvO&7&s zMNp=uHCZw>wJo0ds>xOh;Xi~}ZiNJ{K1H0_w8%A<>H?R5{u7a*U&AOTpijlK{1PZ! zsLm%lveK`EPM71E@RE51YB5yzJ~wHlzUtmjJKrcFa+X_uJw}D0y74~l*t(&N_eKZ! zNC|l5Czm6|b%j3e;#yZix~|A4Z%&3zIQ+;o;L9bSLt0diySk!!ybSGdhEa$z3e^lz zQHH^xDQGt><#%P%=>fHPztaP0r%RxK@tTL!yL1v2cv$A_-rnvb-rlZM{G*CB9JEeg z#XP2{$hK8am3Ul0$T}?t;0Xz=!jekvP^{%j84K4uDdS?kmcm44pO3jw%~O)9faWLm z0-B2oWp5z40sXTubOH^I8_(sN0?fqwvTx<)@BRH%!L4k(1_vLRmqK+zwQV!tPb#Fg z%`Jq?H=#D6+I36zJFIgZ%yxLLz5aOc%W|L*nHM0KNbSwvP_X|bxNYO0&Cr`LPN}_x z$APsjK(1?VMKeUB;E9de+eq8`$vV)t(-wp@^|V&b+w{?yu*$V}l3$^RF}}N~%9Z@4 zN$qyV#6n@x%)MACslA&QVh1!c5zr&$`6HfU$G9OEzB^p+Ol-lv2dq=P)*e019R#{JGhd0}uH5ltp7#Enrm3_8H&uCxnx zN&j+5hyFaBxj4(MQrDSE29KR|;l?ii|HGxLL}`MJvB*e1xk#4XfJ9GNt7^b^4L<$t zpYXcnB7?&28}_(3$GV7fwfw>m6vYlP`7^?a_MB>q%pU4}~##gXBVI&I5f3X|gWD%xO4-sH>Q1s~@DpTuNx@)aLS?A)o%J{EZhuTV!K_VWA? zifT%4-v$6{ATlz=@xfvO@V%ocePqoV6Q6$cbCTlMz{>ZPV3{}kfa(JYccb?~na~;* zU)&D2EAGZ>Yf&}8L+S&7pjCt3EBvqk+Zv3v6@Em-Le`c+0FMf&u=Z(y$J8J2Q)&HP z3-GvrYO4X=Y4{0MG9OLUIyM5(A%3FPl|ulY6cD#`7l5Y(q^y+;?P)a?ewwTp$~pY3 zIuQV;LZC?D=hPJdE!JVMFX0{Hr`0;82H<)12wYZMBap-J3+hvVHfuCf@S@7V^80%0 zHA!(N16 zQ(T4Zu#U0;URSH&vctNa;0+02r&SD#7=BY-2bW#e4J^^O)MEgkMV zPhr(MyhXjsDj5iK6~57{656rwR(F94hi;7U-()2)XkC&8hg${M)-smVZ305pwHCnb z0xGN>(24LJ0xGSE4FGovsJ3pPO57zNVvWlL*e)PyMWFxTyCnf}>j4x*_#OeXjqJ^= zg;w<`(%JjXJTA3|zgKTSz1?j8;HCOO=JY2ykKnhyWqy8E7oeK7)fKHUyjR46RuAgI zFY0L&NP)GGV!w%>N!D913E|)6^m5daJ!#WKdbwpa5iUNHGpb{(kCB9MhPYf{osJe4 z4w!EswDs=RL>F0f!G_9bRNpK?*h0CBInR>?0&JmNfsQ_$Z+5~PwoqOe1JuK$jl>p; z>?D_)%Mo+Cw_6+%f7nprt4`rw;~a{3X`j=*lgj8_~9WYcLBBO@Mbj+1x6-RYNR zHYh?&HC(d65SNj3;RDPX{N%7Q1G+EW>4BD4R-C|`Oy-ez@IPIc$AQjaw}w?8kY%5v zOT`obq~w!^_keuPKsCU5ZQ7Zp_<$^W&RfBiu{uqOjMay^_~SI*XOy|K1abnJ<}mv> z^t<81ea0c=1n_jT4ni{M?sITK#K``jD@2$22Ena7jttx3qkV@0NP9U(c7eTyw9Myj zP23WKkK=sYvI!dHZ^3^kyux=X{J5=Wg*PoJzoHLL{fAGI{e9W~0?jJGMG1q$*UR|? z>pryC@MbTX5;nIR1o*A5*^g`ypsh7M0d5pvS!3AwZ50r-zGR>OHvt9KbZm!&ZxVoy z{jy`eSwM;PHA*|YO#trZ9tm)ZfIimaw8ys!s6c6rT!>2K5C)B76raqP6yH3!1n0sf zND40OA)yKY`77uq56_LqGH`1Z*tz8Jyo2lav@%dEg><7@Lm4t2qKu5Je*=Q=zkz`_ zr)PWaAJARQj*P(x;mp&X0sSn11i}Lku8>3|b&NMJ)6P5saeyUK0*G24~=e#cM zymP;^q+TAfxGD}_VmRQ2rA_GR4k3a7Fy7!U;XF%)NFFWWE9t)Ae4Yt$#FWuD$I!z(4+X#ZMv z^s;m{gs^>)w&g$X_aos-GXBfYs}Sn`HX3P4)!)d&g8HnWVbpJxQqhEBLwjRnbWlkE>FQF;c-^Fk+ zSDg%cJLw8N6my>Xdq@w~Erp=(C0(hfV1QkJA5&kgEAgzqpL9gG!L|MY(oy+(WPLl! z7#Ax;_39rYJzIw`maKo6;iPnL#`_5Q3-ng#XuTY7Zqg5;HtHW^IL&$&)AKm#7QF_e zzxpRgx9V@1h7QuJ^>&uYlcd}9vuNA(PczK*`iG&QpW!HPqyFb~(9e>8pI(~*`tLm9 z(ypIELDxS={tn%aaa8>djwD~QA07#sLu#BAvd+R<*@Yqo2qy*0QcZ)}o(^OM!I)6fOEM zwdN>Pj>Y(*pNUw?+SdTkDPm1l9*QjbxriNZ{g?~zg$!`FTEEl-d@0}=>kQ;A`nCA^ z#2N$Dh<+oW(|W5CV2^;WELLRnJM~R7296GEGWxv$t(Sgp#K^QYrGo$+? zC0ms<1*ID`WL%69G_^3Q1l^9WQ8S;WNyWu@`oo?;ev>=e?Ks~FdmZg!-i`P$o<6D; zC|CSpJpB`kF(?RUDraT@xg&OruJ^Deip1?r<$Qoqa{LTXOn$bDpc7xw} zupDTvq-Cq$Ii7hqM9?3^zAS? z-8c6_WAZgTFdAyE*Pw+CfX7jcEYtoIZM)&=?~ys&Tm1{cvjibrZ;cvhc#dMYw|XkU4uTP;y&MYM@I1jt z(_V{=HN41);AqqSVg$g;za0(hW7=O4yh4EMt*=H2H|%^6K{uIp9GcVcIzh8(KMkwd zu!~|X=AdQER~QXk{=`u&dl59U;U81r(q`HdnVFd9Ci79H%f*)AG78T105&1TW?dEgi%$7i!uG<-pqV|?}qRB3~} zP}AbGM`2LVu!k~`98b$fhTNtg1vs0k3tJaY=FVCu^}4J_-XQ(tzcW#|*Z!uK^`ov~w# zK(YCZ-mD~A|KdQXt7}$YVzLbV2^7BmWzv4#1I1te3Ta)m7`Nfpze?KDM^9Sv}=nB__%`&0B5(fbsOq5LjrfgOE7jHkSv9c`BY+>;;Chvn-j+T#6N z(MKh1L5ugQMjsbYU}PW7*87CI9ia^ln$ZDf6ST;>209cSWO9+wu>L@;M~4Vl?R-p^ zq0+t$LTMH1j0H4x`I+QPG&VX%mXf4Rrao4Bcgl#!Z%$06FCt zQp@a(ZCRYgxtRT$VC7==JA&Etc(B5}y~Hr4O?e7@Z13P%b{iO(FAayrH>{?q!;J?m zM#CC35HyZ2p;8TJ$+8x{qe08G_GQRqjLYTVg#q4E*iE{ z$k5{$&`qTMdL6W&VH;_D18N%-B-n5p4X~Tk+ZnS_gf!ei6OCQ!zoEz)?qrZs2}Q?v zg`918-=Q<#&&D>{Yn(q9=(E( z_c2YPyTw!YSq0hpsA=%^x$JTo<{*qI>%N!)hk&N9^54)2h3clt$x)dvgH9XCiy@Hx z6-EYinZ8FU+#$l+_YQz7ti8!~KV6sW`vvU&o!ucHbm?~w0-fjE2R`B(gF3^fj~I~Y zM`6`<^$kLD#kAGs$c)zrtuX4kN%&Y5yBAtim+#}dD*;W>F1!k71_;s3au*+x#<@l= z=SwF62j#)o7oo`Yf<=q#y0S2N{dLjd zyq9&S_6-Jo1nFIJXJOHjj!Z6DnE*PI1SC~7$qB<|Uj=P!6l+PWQ^PTOw? z*-e{VT-J*YyiE*#C?w3$WX^S&{XysRLo8`V7<5VV^!eA}S3U(keZh;UT=&Z9>2f`h zt4V0De;Bs_n+$UY1}M`P&p?;~&5+F>p*=J5 z9~MRjp1G|Md>4dV(0#x)$>xhgAX)H#J!+$hbjE?~y5se(iySDUqawS5t9 z!3yeisQS|GTm-P)Ywm5{Y!#9QQ`!SngSp?;lqn%SQa3Z4QqtA>CTMVJ8R@Y8g!~Zc zi0(2LbT87A^fSm#X>Za|x%{iNA1_;v>mM=VEv;Zhr`)Ug2lD3YCjBj%ap@pd_IBOG z@P{yM?Rs-5=wYNg^z*PPrNeny*bdFN_e-mo+nw?qkkXO7y>6F&A7-L-wCG=P3Ra@D znx+WXH+2^CgmLLXE-dRT{wM-i99?$U@0?N0X^{qZjB-dQ{f8dW;Mg+-mkvyy-lU0B#zY_dS6xUitJ zxB^4OQf`_f&H0_heFz&|*sZhp@&SNT1)9N5`4UCxbk{W(sbUs*g)jW(bQTYxuyJ-qtgWoJ4Wf(JqZO3D7TZS5M%7`7KUOojjC=0eJwoLIZEW>(O z101JV$~wfi>0-w#ULF{Vz!|C(eMH>))B#wdcy(#YYG-I?DjJ<8E13sy zwyIr=LBI1w7QkAyH@e0{m$P80&S7}*?#BN1wdQX1&9x`(yGZoH;Y?^-x*1?O>t$g((n8m!;jc4O0IYF zJNIG$AG=+~zY)LlOjn>g1V#PM#6CdV1;zbNAuAc1gs6v<-+31b7Q4?aUB7cb73~4_ z8dBEmclr$gdQiOs)Z%x(^Z`Aj5=dFA-{Eymu}9RcZI~PRhZIANV~@Gx|6s7qKV&Wo z<8havH5lKaF~y$n7)G0aC_ilzd(vg7@(cV!4uuhoJ?%35J;7)$2IKFZL=P|$y})?h zV-$lim=*Vu$FRXDLYs=c>M^>5u{Hw6YaXKrjFl{>H#|lm7@xpc#@_T81z=oBO?k^> zsU-}e|< zU|hfk@lTJ@1&sP(V0_>*d;u`7WD$PoNo0WW>KHKo$7A@x$VH2cedIBGV4T2e_|#*V zVC=!rA@(nip}_b%_4EsmVSvHWdhAP&QNG?kq}xm|zVaAlV5}Yv#6~#BOi?Ssr&98v|n`tV=@~`z>~-W<5U{DOpg%+ zBSkZ)Jw`4V^U=%2x_FEnFeXrOvphyuhS3F#Y>#1q@kKTmmdD5jV-tGWSXYmc1qLra zkL7rbE@1GBrmpI>X7N{)`b6%y70chH61}hI2+=po0X3GMs*F6E%V=GMpq5 z7^@Xjnc)QH0)+)tXE@o+`&dDd4CiNB**Zbd4CiL*{5V1Jj3L9QdE?z;3kFjeLoQ+0 zIKg8WsSLyPLl#A^j;i>D~!W7UxJ zIGzY0iugEHa}k!Dtvf~njF+BpleIeqFj4tn3^zM(Oa+*vu7(N*v4qhCG(|-bcC~Ui zh=|v#=8MtKD`#a8s6j>%QRQ&35TC9t-iRONoR0d6&r~zvZ&!76`v)27RC;Ln+=qdbYbkkbIf{9tuDP_xfD0=;*9zI$7Z&shc28DFS2p|5N8Im;&jH3?enbMA-k#1{!_!`J}^ zA%2*k^%w&%EsNE|=O7P0Cu0%N;p$Svu-WGvT@AEE-2=4E=e)q!o7GD|+kJ8(C4RJw zc-noU|M6w&b+~o72)&B82-@Y2c;d$k>hw7$pfSdm3)<_GJ~@7}+J#WG zf5@}wP~xX}7PUO+A2Nsqe5%JN17maqjMF?uY0y8E-_we(^cW?O7)h-;!)5r2!T6C? zw#JjN!JrY2pXo8WgYiC#=PZv=1jYcSajnNF1fvbjCVq~`C;)@zF@CPc$PfB)ppRt$N|88^i z);NW}H70RAA3t%cirLy~?2{cY4sK$;I3{2y68M7*T z0!p-T20>KW;fVlpop4;)-O)!j%I6=OlpTP{Y;0ttEy|`9YMjM6)M{LHJq}_$Z}(f#&EN;c?Cz~Jc4b?w$b%A9!#)Z*;8i#%qM79gZN#? z#zQzH?@;#HOxvLow_>uU>3ZvP$l{FcX^@_%us1_CaWmG`pHqw>y1=Yq| zSwGeKGZ^y5+ek;`e*MPVS-4T1g;7r99i(yV6LsKD(kb1aHMpJfO?n!J6ODJ1->mnN zzK1j}U}nYM$IIng^<{%WKS24___7ZaV`kw$k*&;GAnlp8IjfNwcCs_WlyN_8GIK2R zqUAK}%sSF|hkO}=o>^23`Ajt)p(SqO)JLB-2^U1)Owy1ke_~th4fIRpO5D=(MwZVj zS)e74wmuqhCGO_ZOh|u)W|O#wWmTc8VI30pQB5oL#U-F0$c5i(orUI@c#u{*qG`et z?WCjf#lFPDyv{nVBQV&BM>&mb(%YboiN_gcvwT$}(ZQ){OEJ|k@swNL<*mh>btGh$ z8rs=fyo2x=*R`~@_)a7vA$!$+Rnl7g?li!^yRJQ3iwmKN3E8pss~)Yz7qSF)xUR*m z#l_IG1Q$i1_jW6$^QRxrN&Ql!&fE+`GqcAbYsgrPr+oBl4TvBM(0y>o;(hsRCAx`O zdAwjTpea=noE5njzj`r}J8QgKVf^UiEcuKR)@^5^JZ4S2gtC>hrufc;?0cx_S@kcL zv4S{joE4=%tvR2Z)j$zkXN?@qno7PcXYOZBlXWLV%jnEwFl3oE{c(^u@d6Je_UycR zhW&jLP84+Gf`g&+NavHrRoSEqWFf*Z+sXk7d(dBGw*3hG^;-m1@e3dgeFy0tq_zH% z{z^UsjhhdVzS*U}fVTC63qhBYAJW;B3H`8)N^?2s-sD&6!K5AXtKBt81}UqnNHt^4 z{mn2837bo#&T(U5BLzTs{TE9K}fcBtVvKQn9><)k@`1kC!=#(_ji{#B zBcxmOPcuM2O1f1)i;gh%80pn|H)!8CM`Ua#LqD~LTwdXw&j4lnjJ>CJj4 zWu76uO|uV6(6b`ozeIY6{wM_c71BHPQm9AlHFm4J z?7uNRZ;@}{n+q^`u@4BevIk)p82gYQsO%!BZfrM!?RJu}{~-t|yFY@7eL^j(Q1&qm z0G|?|V`XEGea2{_%1&T78T*_rEy@ltv@d9mRxA562Jj{AX`34MDC~9Y>qex;Z{}O?01xU5yvM~ofhUgz<}HY5vpsfiIl~EPKCb z)4+@70{`gBcL(0hD*v~Oi`FY#X*d&%pR&$^?o_MTFI@I=b>~-CcWTgY#C~Uk!@Ym3 z^FNr!h?e~BV;RLQ4k}`XB1l=H3`#(gl|tVTGX*qTU!eWRd;(gmf3Uv%0$ME&QDPYa zR$HP$0Re5+C5%2(z4y*yco+HE;?0NrFksnZJf0w_?d<~@#7Y>^D&ZRI={hEsPz zA>}-r4`j=7V1;s)QLl>yRVpLM*R0^%XxA zR$uBtKLM4NC|!RQM>N$I@5qQ%s5_B_2n@>WC>1=>@n(O{;vS%wxa>irK@V2P!b@fm zXgm2?2WUMNYM=bP>vd2_y>tTTFBgE;JvabTH-Aq?9MX@?>gkD}foN0)S^Kcnot2zuB3TSp0*&hVq7*Wh(_B;)GqT;8! zvia71Y_eK`B$fXjMeMP9SwFP+UHe#69RdgUU6@#dOv`Y;jSnu^3xo?ljdUb_v)`)$ zZ>D+_bU;6ar)*8VM-k~>Y21TcX*C80V~uJb9NZWZYCC8zhFNY5M)q`S@@z%_*@un? zJy&&KhZGJO$rR3$)m@9jpV+~&?rVf(fq1@@jF-X%BE=MD!)x}|JV-aGg$(9A(81VY zvJmVBv)B#B&D-JP!2R$9ea^*bHr6b2OKf+6BYESpy{qBbrL4RoJ@x0r9v`GVh0i%s z{>*GZH_9_-HM-)8ITMP`g9`9!g*g*xeefA7=+m4@WCyj4o?^~q7MZR20{)ySq(caV zR?8gL6Ri4wBL09bfd_Nj9K)DfR1Gro5|FFs_8D(DzjOtm?g}Eay0za-Fhpm}A>PpZloj(tB^*14Qy!^cT+?r*OZj3!idl^6pWCBZM32QYpLzI8Og;{bx5bh9xh0e`HxB?=#;{HQIFO;OAn`6O zp7{^s@t0r2ysr!Ln)fWY-ERbGt5k8}MW_L;z@&P(wb2{lrFzQFkzR##r%E}n4)S}R zsWOtbejgQ?D!&Y4N?bUNwwvljeuaF%GUbqVD`fMw0aIs{njcCe+*#heJF z(%XW~#dCTBrnd!~i@zBInBEqI-yFd7wqSE{CzL#u-WF^wJ{k=xHQo)mdvozDmc#@X z7Bv^Y$O21m3pUHwM^lqr*MesGvS?}wElP`gSu|D8FN?Oym%vg{-tN+-YsVslshsbu z*GIxQrlzw~*(7tT)C|&_^<8KHDeUQSg!XtD=s4xK>nikisRSE9yS^8rfz&M09s0|G zpl4HNhs<$Ob2!J@DTR`@^I(3gI==q#y?a|yCN*zM} zUYpx)sf8R*8OmlanQCH(iQ~5XyhZ9THVquNH6KO8O67-8f4KhyPf>BM3I^rDfZ;-g zGzVovw2k=u7)UcnF{^B4sDowt_){OnZx{BJ_^N6R0- zpg)X}*0gJJ`DzmUErex=uYVEprMp%9(#%UCA#1lAB@y}`GvR)@TX9s&_!Vx&QC;Jk z+=}}aN*KSAbV$@Sel^vsLO%B%zlL7;Nq~kh+ ziM)|?O8dzvgFdH|6u-PIbywj z68%m5cFJthvV?iZi=a2_=g`FBcalbv#4~;u>FxSqOyc6(Nw@32V_X=&hjfP?Ivn)9 zq<3f69YMDne}pue zAv>%1V^aRgo{6R$@1R~`Vs$I4;7QgUCRPcS-P6oN2&aG<*fRu}SPhs3@GL>4vLBcR z@EqF@W?6557k{4ViYR+4X6W%3SXr2IHRS=kNDx={Xjbpb6iX@lWcqo9ph?-6!=}Vv zC1}P%APheZN}`4iTJ0BOMq|2m?HiG8a-xfJ%PUt1M>m{&fYewL9$r=QP)dlVYClJ z{+Q|9u54Kq|CGhnuIzRPF8`uShq8~D4e%Mk4rRaG1n>o$|4zPOz?OrnF;Fh%Kbf6x zeCyFPqALCyw<}~`7ysRz*CqPH{ZAISJFkoHlX;ybcF&!@xf8p%JAF&Zb=h%u`qm`J zIpXg0ty!jGad*bnB2%%rJ7a6LMzSW|8QW@W9y939*xIbySP|}wZN1FP;+8kD;{-C^ z)tlIHW*E=)CU%@a#)AU3%givIC!k$shVgC!IxH?J#PbF0u(+%pFA%U(rlaxh0(Qy7 zF5W{XcDo(U<>NgCXeHCUc&W^zD&47EyiBHY)oKV{IW*#-^gJS>WS2MID?O2jV6s2bI3J6*G1cMa6 zpIBk>C{}!k;;6LJn%xy(D6Wb{{mDD9@nI5lM9$X4hpQ>BD}r(@gF?2{Xuk9pXoVkaP45=-LH%IWS-+7Hbh2U~;DbEw|w(UlS0ty1{ZJ-%zh!3qxe>z+^u8rht%j z2n<#7ZN+1d6`^B}IMztMBVd4(YjT(3m$wHR<}?`TWTXXg_8tLpvzZ*F23(CuIV(tx z_G*@kn8|}=Y8o8=JnB3dPV0(2d;qla|`U@27jjb1$)n$3_8< z_i#^pxFmI=hkMw=%lg87vWrdZth`zWJjKIBw$*(gaD$XU$od=|QF5Ak9wksAN2`)C zIa*a|9h3u?MmeZeZH=1?kPw%MOgfXZ<(N-YZp}%~b#hh?6!EQD0!5CPU|)p zzvR&Z_Q+-W$zufUbp}x-j+LEFLpdBdB##pW+i+4YP>Ue!8?z%w9xn(BLa&boIzbQ? zgg%)AbfO?E2=P;^$rXy((P_uGA?+>&}P>XUdL@_7VD0UmII8M)EI7`rK<=AW* zXA5dmeR!xTxmM76yrae3o+D_Ja@LInI#&ll8zcczj+7YN#+oNN?G@Qv6r z6+jyV?NQE~r9hWRZujDvYS_r+rQ(J&d|Vt%ZWM%j4R{tPd4(XnLDiFKxl;V$UV|!_ z<>Xa@LZ)*S^M0+UEABOTRs-EEZn)Rr2sW8*iXAlWHCVy4+$NF{)A^b8aJzaI776zn zJPsS4yi;*76*rw#Ov`q4%dr@zm`)Gs=G~$NO{NoN-tUnZnoZ}e1kk;b&K6u7&GNiY zP%Ca#pcXu!IDcAgI%{YF+r?j->D)I8=pjMtO{a|Q;t};WrZ%`XdW1iD%yO8ls+*c% zEvsUOVC+^EJQ&uss_z2B@Kr4X$6vK~pM(mV8h^ebcjomw3X`ZgwK zm;RV(yn}XSw_I(Nyo>Ukx}5yGe}>E+l#K)TF3e!&wcg2nR0%JFNZwNgZpo(P{WSFY zI<}byC3o`4oaDn?POZ?7veiB6&Kr}Ml_X^&GNMNf1O0?Mqp7Fe>u_f@4iqT)6lW_b zy@*!s8IHS~b>(c(f2Ub#(KFZ!JSQebU(fcjgY;@S?VNl8jRK?I^0s0g0Zqz#VA#-X z(@RSMUu9y~BVX*F@lHKDSu=ku9CmgCqa>7kn@h~HLzvvfWnV)!Ad>$Ow(JBZ-(zmL zfcX}@m~XSWzQ3EkE0Z5iqwia5!PmVd$%foeKc)yT2gt{-IlM2NKPdx= zrARynRwM_hdqG%SZA=c9rfyhVZcGjlpxt4>Q0c>h!yB3SO1CG@Dhdtnibba6aIfbs z3>n#dF=I-OP=`qrpIpX>7{2+|ENse-dK+YSzQeRyC64!RMuNFKnK;2+5aSWo#ECrN zs&z90POM-?g6@v>$*eHj-LY;Z9nu%UkR?tbU7>$Pf0{UrbftWCIB`06rmJOVJ+Z2F z3#YRAFrSGv{u`l!Hcw3_&f=;~g|cP4do8!SD=~j#%;zvgn5CTtJDO-KgiA!(>j=)H zSX9{;)&X1~+u!)KITR>yAwd&nY4DnmrrV;1HKFnn8$?O8-KvIt!*aO9RhsU=vJHNz zi;I9e(cL6Ay0{PnQF8?Pw8ZFKlvQRq$mYbROF{GEw8WJx9z%2gIdK&?`n7HXCvmmA z>0b~9eGLr^;THpg!kEbe(5{0nPJBDc+xlUt)rCYS>wbO z9yM+n@d}H^&~2=iSGn+|MZC+VP=&BSY?AY;2> zzR55{vJjVeORCX574VLm{qo8lORyH2*yX}9EHwQ#0zuc5AV%Eif~RcX*MbPjLw4@@ zfb)w`wUSVx?l2~gL)pYQHY9h|CEUXnS*z@G9xV)g0n9mbXtvZUyVPXO0u_QAa^dYRv^@iC#9AM!xfRbq9 zu)Qc1`4(JaF-b$83*(zOf+d7Q3^1&TC8UFTS})K`N!zkAn`kDD+c77Ae-z`XkgbEn z(WEQ&86ohGVcM&UT8=x>NXSRcBdEMT5f3)5kO1@U{t!C$EMZXd1De_$(0KFCHT@zrGv^Msy*op+o5vz(L&?Matus05nM&KSr8$^-nVjhui94b{%U`2`-AX$yW z1ZJ6$Vma7V>B6E(#d7FtxC^aG#d2tBgg})&saVcbRk<*0QgKB+f*R?=tGB7sBFfkYnl)}&~J2}UGpkq_E2Lj?=MqP zMNevW?dT@_G`Oe{<;#3lpH1d)@xB{Po`2Lb{;6y3gvT&r8!oYJ$--ACB$O%%u1d`lDmxU-8}KB~>*YP6FAeD#xir7hik1D}Y1{LY1ge*`tnl z-X?jD>YD7Ey+o8sBF12;V|=rr$`u`2fX4p7Iti&f;G2La9N~pgi>Q%L4!3b1__c;H zF^q~<3#4q-T8K|Nfsl*zlcfR|$Y5oP!^&JJ5>r~3wnJrwY|2gKH;MNt>sa}VL>@0p zs>3ApQ_7f>#lkgms14Cu>Hiom0j>9v#TyLxr>>cTGHXB*j$sO3N7ASAW-WCrJ^D86 zK4a<=tj^DPfXHJ&_iYw-HWft5M4t zpKrsiGp0>oqOdoFrK7HRHF6OyDo`h}H6;m8TydD8PUh7>*8xtupE+$M{1tF&oM}46 zP1EXCQ};5(r@CFSu>73?qox)B)kA{=84r1?)W!GD*TIeM-_DQ z02ylSSGT}yi~!rWVg0Jc3J_1hAFP*wLvBE27$MA>17azN*@ubX=2f$f1#u8M+V2Bt)%k3vJ7wm%3|7Ags)OTE+^#qB(;+j^%CGzUX!L?OV``9 zXQ&acQ%w)fgYc!S)i*9!V;H9c&fqI~>P-s2OILmcOua>TQa46K;kQ4`z~qkb5r+Ib zlkpy4Y#r3&dTX@Jjuh4*tV*@IF{y34RdGu=q*G(Lp~MYToUTU}0~g zC=2_=w4Xg7BH5mjd4-*`%2;Xi{?AnHa8q?UVvySGTMCMQCSv&PrgQ{J*f)uwl?3yA zHHb)^C;eA#rc1Ey4X-8aLq)%UU{Kjzd~r{;QQE53sQcKHDZa-rK6ho@HYx45uBTT* z)o{*3BVap0|KfEus3`O`YPeB1$dg+Dxd9?K+eC)yCLZ8_w(H-&K{EVbK}_>RPeKsK zxT377y0mgL==E;UIVT$dD9q_b?^Q-`C)b$J%PR!R7!!EK0{@8W-t)i(08e`c)6g+kp@Ux`MR+f0YiSS>g%Nc+L9n z(7xFp+S&sISN^wPC?r`zh>v-KUm)76T|uwLvVK&?gnxL>#=Vg3;*|?yf9<~mHPdFU zZnh^_1YdhZ5JtIdzw<)Rxf0HqcX?`H)SY)g@Rc6>fTHTX-#_1q=ifB=kBK0HAHCn1 zFGut52skfwoyC-Mk(Ga|z`xK9Kdqd*QO@XNRSF-qoj z77GxV%n>q-sUIDJX5VKzI9+_9;jZ-5R~LXeW8ax%?yhv1shAJl$+mCsOx;WJ79o#C zBIXBghW~)(+dWp`e@{lV!(Rc}lPhbft|+EMN;vm8_=zU;@@3}R z=&k1Srct$kWEY>|MZBQfLd;5L|AxRg{0wq3>u$3h^3Z%t)C&}chH_5JJ1sD^57}&=n^fGuBuXxXxcQY#53SRWz`L^DCOo1MbgQFlP~(`51QWw3u#sBqJ0y z7F>w*^x4U-oVLmJxIej)L}S5yNRpjkT4)ImDBQ!&gG3up%#5SE4bbW2hK)u>@^hRt-dENkkEyDk!D@e|G4fEGJkoOB&HXCF+NxYKcW?~XT zH@h>vPm9Rml;&IPhFJ^=Q@T%c7?NYA&=nfy?+`GTeNFOeq{BDu36d8hYX6&(^f8ou zCIN0{dl9O@*LooW(t}}Ce5X>_&;RXMOJ24F3=h+SxJIC0n4-cGM;?3 z1um^wFC$Tz-4)czSe><;Vz!zDaAsBu#fsIn;GLDV3oRhCRNVp4mNkGb{S@nOeb%=G z!_->koqA0=8xO8T1GN+q1F>sxY2Icab$_0>CIqTSwML`Wd6x$vk6Z zKTNr+GfxH6&b)y+(%s0~LekLX6uyz!)w&B3oq02J%YBk2@B(@l!ndfHOm^3HxRJ~1 z6z#qFE6`Et$kCun^rA7K z$Daf`q>o}?6PctwI%f#z$xWau^s%|1r)~jVsV_jY&@=YqbHY||~^J|M+^R}{;)kTGWuGK}m>8bkAisUU|lKyxYTF}q5P zL?xC~vQL$U{|!{mI%yQbu;}6VbYPi=#WGlv#&{vQ3re`_%nszjtc!t+O@pKP0P>uZ z%dfR&-U4z*?g#H9bgjxzu^(Olw!3eVyPLF@4T9W{Se-#V7Og$^6Y_06iS(zWL-K*B z+|Ni?$VZ`aJE=sK1-$hq_X{dOwf^xC$bWhC!7O&3ZqNOS@=@JggZ}0_@NpL~5|X zm!y9syw67BM@1!^DT+)A#-llt!{*&}}-JkS6((PGQqzy&Tfz8G)D8%4r zNN?s#2(&4<{A|>V6jpEr3(L^F*e7_>9r-N8S%sim3xg~~?ym(;8Ii|AyayQzo=Q5T zGob{*)80oxR1_R@+)^XBG8gi0AqLNI3(=1bAh?PJ7txz&_E)=w$UTbSnXgG9PVEQ! zET$oaPAr6U=3R`m6?wWTl8Cfz^{B6p|$F(`+^=$nbo@AVW6wNflQk|m*I>gf4$}^ zLEb3RoAi(*=rNadV-3thX7Z}Z->#oy+G>LE+pZsmPUqE<-d*%N3Mg+ZX|0M@!W!h& z#d1+Xs%UU6z_`!hFRF^3hEnH^-vY4~Rdg(Jk~e{1n=0De7hn=2*{zB`hKA-%Cg@Z} z|6n%inTxTox2rA8@%Lw8G5wqy18h}QEI09gqCJ35fNhXa#O?5j6rJZ>Y$auGdHi0fU+~`(Q zjjAyUmMv;Ey4A9C2+1vU-NLN03Z2`CMaHocL09S)6l1r#ACSOm&C8Cuje87qM5?u0 zgw={`lot#}O!<8IA~Oy$m|x2}F5t`f`QeLE9qy)OzJ!SvcXz{pp<&jBH`S$i8*K=r>SeLA@A)1=aOPvn~5*1vNb(6Oyyw1+{O%D~?@E zh0Ivq_gSeYO#@v=ezk6jgPuY?j>slzLDWnC*J$ZEg$z3LpCBuO^R8z!OAa@J2mgZ7 z^qS#(mMo5;paO%3oQtMmOSHj-3_fI-%|j7P(X9+-6so^EIAb4Zw+Ty4;;kSyu^6)$ zEa1mzgYn}foY76-G?I@!S`&1FbcOzc^0OE|Hc6^SApGuGP^QcwAa@o9+dwm!g?WD^ zv#^_+%yVPQ#%~EJD;GwO;-uFVWJGx&dqh(0REz7&z$SAaCn83bH>Bk@Tf&5zNPa(dhgocTJ)&Z1Ku zL!?qzMW-?|hUObEMW?Y9XnhXUwrC~kATv{R#-k`_TR(sjC|X?#`y0|zr-5E`Jm?C2 z0qlFxnf0J6brw{$=&Y5XtEH6}t^E+aLPURuxQotZCZm$cqIJwIgsB}z!TU-SA2?i zH|xt8&Q%O&o1Te07hTP8w!3##UdyJ?E?a*^*Z)8EzC66G>e~05BU?T>35gOrwrnSg zVkaJD#~Cd{0?y!IlgSxz7^=v&JT$gtB-wFj3xP70QpN%WTEbAuSjIBnHqb)D)H0X8 z%WYFi+jO9%Kp8sme!sQ%k>mjP-FLt5zVH1}J|eGm_Pq9*_F7v<)KdHGOFK~KTC(NS9q_WmUEzpH~R{|qfq-m(HtnS!uwU_yaR@{b3T}2=6(#H zw+&!xQS$}deO^i_TK>7C$rK+g0=(jnpsl*iP_&B9hffqqQ46W(m+w>*tyW5lMyjaw zE_736zfJfUcG+M*unzDV!r`KFj8(KwO=DJb(Ti|OiqlAHJ=*;G#FJS1R zc7@JpE-F0&=thN>HW&SZ-ELB7Ni*IXy8tVibtQ)J+I#VtFDxvY9j`3~2jZq6Y6SVh z0enZ3HE0lg{J?{oSmTwgSQH6qSUrDTwF}pC%00HO9=bz_b~tB(h6{(Qjo1Y^IM>trqu>>Gb8_pcw~)%G&6l{ zZ0&`DnmIKe5`Y&TW7b}qev-Ad9v#+IU_88O8J|WD++M|1jS)aQsc5f}KcfxZH^^&?@@KO9uzgVz>IW#&%|)%Ca(@dXSvZxRIZ398)<6v$(=qUJBIn&lI}^PWOvl&5mDaaV-!G%}$rcRyz3 zlVh=?Sn{j_KKF|On>C#~9W<OP~pGJSyM`#=LRX{lM{shn;oOUc=o81TVGu;QJ z8^xKy>8wJ|EXCjAaIlzOt?YKNghiP6d^9g*v6@kEhElwoQE(<_R3$SC1~?*WifPH%O-Sycf3G`X0Ik(VDk<8vyXlWb^PFbKakeH zqGv1GjIUvA-i|Kw?wNtY@;_Y$Sgy#Gzs07?T`T`H;T-W^%HQVfZTb3S`3Kwa9IEg! zG%nl1eyq=9sIsjBE(Jl#wh7qV4)9n3L)fvh4gqhU3vj!Dj|Bl9C*TX9Y}xSw9%uk~ zf`A{g?ui1vI|txr1Uw&&C+Yb_@7d zkgTltgy|r`e27X_`3rXdE=Ud;NMuW%x3fj zhG~MLDLeHAw8(3KRq@YV=YhQ!n$BIHF^6;Uq7yupW7=H21W?+3n`=F7GU}I{26A;& zG@6eCDdpx=gw^7lze7n^GzpU9Yy-zv93hD9RMY`c^qwS(syGstGf3jop|2{M&GQ>E zA?NStx?-hy7m%RCV_(H;^9dkTPH{DmR_U$5=>}6(9Ao|(C1GbJcBNviAkEHf2yn%E z(*awyIyYmb741Tac4#U%E@h`bSA-SN3%RM{2j*T((fNpDK5qKZ#BesB0OSetaFEh= z-TS zM>3fmiZ(x%l0HR_=Y$-?&Oir-d0wiGIla(o6)#A&J^KJ#>jaA$*P>coe&{y$Ia7mg zz^(Q-!Fm<-eUFhd`Qe0?}*@ZSiM^;p9a!FMPNkV?g&g3oI zysvD*0xHg20}ZH1c9tU9Ghq3OvlYp%M4gIr2;263obI_a_C8zqy$#^U+N)V2pO5Ns?{j_@(9qp zC&B&~R(@#{c)ega+HF)`pr*wiFt5B&rd2R(R9B%|_ctkGQXS)=kwLalZigu3!7 z+MjlNqYe0Kw(GDbVxg5^XPq53$y50aKJT;7Vt>~p=kXZ(^=80(!-q4FMp9K?OHzSj zpNG(uoqrq!xhsw`C2iRxNZUKDCWIXW; z$5x)kIzju1B0N8xuwLI6)%vIfD$k%w3)?4SR+VQhK-*@Jdh>Fl?1Eo#4x2Y(R#o4) z5f)WwU3ER%8WIk!x?wv(dnHbARU%D3(7ozLuG|lbReg(45IazH6QL@hdezN@8-(gr zw-657^Jb#-R>I8%38U&ZLankFRkw4zcG-)nJGd7eLer`{`Fw}awCXO>v`=VSb@$#Q z7#bi=tL`OD_ZZeXh+5f&cVP{A-wdKv)fwwCpwP7HOhwZjt*C$2rC7ghH(^;-XMY#4 z&prauUUly4Leul+06uRP`VR`Nt1j3CxJqbUbx|+i2BCG;#e~CZ{+Do`%{FOWbt!4x zYV(bHRhM0fI(Q$+QovWSeupt*6X|~OcFZl$#$<1*KBEJq6}nfSo1uF(bJUn8d8qol zGqJBq+^WwPaVr=ys=q=C!tXb#FCbKf7-RK?gc`6z)gl?>>1vUTW_xoDdb&hWeJ^2= zjCT7ia89*IMu$+k`f^fQadq_-oYAnXvYO#3BW3@E^{?VCjoEx5Z}ruL_t;Yjf1U6- zb^(+@^>w7gh4zmKUws3H$BudjX$F7;lh))~McbE8r^Gz3R@N18$JrtB$=3 zI4rwY-AlMxt*f8wYL(rq-pM()+ntz5b*c{Y?64oju2zp74|s>&dKBQ(P6ga&SOKs~ z*_Gv}T5|p(!`NRl**rc7_5428RWrprm1f1cDi4UyybMijhrg>(Gu50A-Qja;u<)8` zV$u8#RY*;~ATylGRX_?P1sZgyP->=|+>N#*?!i}uxAa2{a$#~kFsS(uk$UyPGX%|!=b zQEbDy7JFTG^<}72@*%*nnje_Gv*CwzB?gR|Crl>qHOYUNP4fMk#tUg%U-#u*9Aae|2S2>TuF4ZV!rNQ|d=*Z_ z_WO$g2W{Scu6=w1;41qRNI>lmQ-B-nrTDCU@)CUF6&ILn?bEE^Z2x5&;AbBI+-h&1 z4*16}0LIH}F_GHm{|p$01GBAtVd8R%>r-%>YhSDd+$Zi@?Mv)uSkzqY%M__GF*LQW zaQ-mN?}Bh;*S*Iz{%jHQMzw$V7RW03&)PTVqaVXA0)=b;#QOMgt`&gaz6Z4O**^x| zYTsRkjrH5lbKLjlqCVa*#F&vZB7eEogv5r&b`L~nQ4+@`daRLWXwc3d=e?C4>eA$#HHD0*U7?gO_hRtD&qzqq#?ekA2RbTp(kD z_?>gXs-cqw2|E9%2XeM_P~~j&0XbKYkh21m34JN|wWG1!4sW%GE|HS3b1eD{T`9;? zXB}iRbhRMOPB|7E`i3B@9KJI-v{#T;XBzZf=sH2xI-duDLf7X~+S{ENb|rK}F4?NX zIRR7%-6+*|ICrwzw*={S{yhiC&AHSHea_+lkXv$>pv`b;a)A-LHJ8`0#!8b5jnHkm zJOk`0?VfLhZqIGOYR_?=UW*3bmaf1d9OSOti}CCV$Laubj|{)Jw68y9gzlAUH#ube z(06jl`*%9ebD0n4J`8F+;M`53JtD|H=Xh3oRNCx!m^ckRmU}i>=sBl=Tk!qdAx!gC zX9+3zc&-Z*QV92l;_}zM7zmy3!n}J_*i++_$ zX>BzFzaSxhle-Bcw3`7kd+4`Pv%?Imf$W5SFW3oY;9c-u=#RN;F!LQ|pkg(!Kj&^- zhuts(?_m<5_i{&o4V$xg-6HhYTxMj(%)p--Q1+4Zvd0Y6LP>{A?{+Nc95X=YAY^%| z$u7hj(4nD2Io=M;WiQr866Jcy<~Nyv`Wj$)Ud#wDDGG#1mkEMBUsrvfYYaw)bIc$~a3 z*GmreS%KATS0U~ER^Tg~XO&Nuy-%!(ryJFE&z78mv4aUumZQY0Xst4?XUvB zBBw6%o`$kMD{ukTz>!imYz3HX3mxT^y|n^GoOP>=zQ+nYLUwQS;uF738|c{#Y@PR2 z@Wh2yU>W<|;Jp!}Utt9<#)gNs$v*700zc*6b_jNZ6su)Z%Ur)uN%~_+$^|*j94< zc|m;6K5qFJJgunacYZ@YJ;k#gNYL4YS%<#pA@fx^jaX3V49`D6vId9O2tsEH5_W3H z7w37%vCU2m74t+ zs!v?%&|T8orOxZDcCTkYs$Jn+9|H0n4>@(O<0rd6;`tFuZgL)E$@iq>PN$CI^qA)z zlsw=tavOTwGYxCp=UhX=J?WVbWWQLU&@&#&;B(HEs2uu{hjE$%rGwpzjnIz;Iq39~ zwVo5?E$1W3?F$|n(hmZkr;-tx#U|P@&jbr2pMRk@jgJeqHG3*FyYD!Gc$>Evy3N!FI(sfGCU6|QkD27sCz8r4TO=jQ#Nj5EqLB~6xCP3g&zF_+>Ybw(M z!Jab%l@74NoL`}r17=_YcVuSHN5Br6fm~5N(&t-dU>RJP(CnOYaO4MOU>dp(l}njn z1#X%StTKlR%(j%euF9dh_E}0@*X2-ML-qJ4q2?l4QP2w9!`)n*vj=5WR$vFIv_!B5 zD-fpoT$ytN%AjnVZNOS&yk_({5!f*~FQBZ|3iPc9wqDBGwJO+{LsbA}v#|qZojG1q z*7)l9`QZrI^T}Qrx4JVp3|Jqm^Px$}ZGOaYV}S?inkB zX_tLx5>!Fn@R4Aex~aPXOSG_V8l_bo+UxRpXt(9CT~{y(cG@S0?YikR0sG~rZ0ibF z0}k3W=5@t{aZsNOc-BV14fae>p)N=`Y*^DbV%!JMz*zpX;OSVmL#LEIbSd$BDu8dq zhLt_ScYKyy4i50wd(EE$an8cF*5}E&)^JV-ht=EWHf)6LbsQsqdE( zzq1^(tN*MZGYsqKDH!H^YtVB^54=%d{aSHZo%ll3T4z2e!yJBr zq0J7XgbA$RylPpj^J~xzFcXnXQF4*ioJC-jDc1qa<0aT|UHSWfC16%J=UB|bu%CtO z*UhEb!F9G8z!ijj_G5DaR}%KyFM>#QRfL0bF08ATP>Z@8O=r``4-9%adiUBKBcJ^2 zKAr|xE>e@3<)Kd;6lgRf1ED3a#00t5_~CYdUYoXc#)`HLM#CSuxf80mH;c^_3iC-t zpL|fhd=xC7K#+_w%`~VK6gGYp`a9edp>*EBd>W20*P)ir z`3X3;VW~L)#P8%50$FCB1tjRK<{XZck}63j;X8`AprpZBQ;E-JlRv8&cKk4A4a)^- zb||wAE6g`g(yB-yXD0bbPaM-dQC7`)3cWbGPbyKz%IZ*Qvs=vhsABekk_1b*a?VGU zYyu|4a4)BSnRWP-)08@BcqbpS;m_QMij+iA+$Ym|9LR*}q}WRH8}j6LZZk^G|8Xz= zj@n+EY&3}<%sI;4aMPG=&)H#i0x#PH!!)PU+(md6xOYys$s2G+KA)&LZ35~phHYdq zyxx{oOcY}=xr$$Y0g|`8342$5-NS%wic0x)uB0$OTrwZKUv|4l4!wp~8iDqf6-)BxpE zEbd_0&!ED}ebx~W+_GDskISb@ow9k5>+)&xYqMov0tw3VEym2st}g;yVAT?S3DYT` zZZYgsb{-a1UT85aS@s#I^70~U8R5gAEXw`XO2S`+DJ(Cx+6Zr44!FePD;LXd;jByX zf=9qVnF4r*bv)tUvj3S@lyFWv;DFUj_#v)rmNiKDGAOX}pp^t%&Re48bJhI6hh>*n zD7g7(z?If+)B*7urt@>7e6BAb%)1O;`&#+DO+N3J&&TBRS^0cbKL046zm(4p<#USk zQzD;p<` zz+ic?heb>^KZZY$+s#rK0uP1d&ewZ#Hgf5@OpUJBC zd)!J)*(%YZr1)4-DedQoz6=x%Uv+CUse5ScOgmlMi%PO6rgYWRM5?nzdl8Df_Gj=p znOS(ReLTn|jTrdw$%k|-AF|tCn>P;d&Q?^An!F~UYKpy4HL;PXbBSArw+EyS9^UUh zWGr1ZB!m)jme+p9E$8(CRsOMg{{eNpb|ETE=H&_b<_gaPqUxq7{0JAvFVZI`;8h9d zSc)%r?L%fh%mJitnY2tl{S5sxap$$yp_Wv?lGT~F@(Cn0zX=p|_TtZE_KWwX6!Mcl zXC;l8hstUsE#-qPeJn-oN@;VK+lDzIAG=4y<8a!-c3M^JPv+##T*axmwXjQRKDH!{=nC3cYrbO#AqtZh9-5@&iRXMeMEy zBE3q^Sm-Nj*Ol&fi#>;+_`^l7=fSQw4=dZY}nMjs#)s$sJ^^^-x{i$lymUGf4(&D`@RAc{js^%W~$t4 z#{tU{A7E8h@X5{!$`90G-(LGlwVTq%!HH3za}{T!lQbJY_l_S(FClJ}L( z)Zlk8>7+FH9j*qq@W^Zb5Db|&*1<~qn$wl`sY=$eY3S~$HF%zPINX9@%`@HUI?uQr z?V8PwnBmGh30`|&&D%T`$Q6W|cjzwQhV)_R=Uu|Kd^@e?FU(l^Y@YCH-eaGB`(;eG z=6yEBYp>vs)O^6_RdR>B=EKhdZm{130c-wBS0ijsE(QEI!p-&tD*^vK4{cje|g%n(Y^%sz_=AXj=OHL-w)_Bccv~c`5Qq<%LY&eVAlP!qtoDG?+nPAeb z@X67%1_=@%e&>{9P%_aRZ)R>=ylF-a@?RsWh>e7E$)Q*OyM@Ct+V%RS84s$Q0B@4##S z1kXx#!H4kGPxmm3>wFEh>kB=rkHb!sCc67wg!jfjKy6_?W_5-2_?6Qn`j%@Nile^p?zHrT>;urI0q&Ue5=izKr^O(%a zYyS~Xyf%Xh{ueP_ukA%C6fiy~Gx1z<1&;l}n%5+6rPlXadMnNgvhGrJk#`ALH&}bl z31}x9QhSafLf%VYn_%s^Bk6*3Re|*Y*r4{BJ$UY)457*U@Us zk7fi=?`D28%eD)!>e^edG6)5)h1K3l*l$0IDb(IZT@bX%r?qn5W{#Z)tz7$UKCiOp zalAXo$_)j$)L45bM9x#v3US|{6G=Y!d^S*7Hwb= z2XV`fa7%_*A|KM6SwS@9AFsXDt#k=1rOSP~(#dY|-RWY1bTQM2a0T5;FQhB6043jb zOa45r#O?i67Be`@RH+)AOooQHN_@r8sD;loL6@kO!=vV(Yz`+$#v-xgo~M*2MrAx{^P+UNG_j$=2iCN>|Q^7W)=51{A(F}xnL-&~rS82@=x?c{o zhVv8Xl+Xhvv$3`l4Fh@5d=rSzIUKR7&_jawoe&y^9+qlB=ek-Tj|fubd}IUpH**p; zw87z>h|qTh2|H~tr=jnewJ2$J$j_mD(x%nK# zr_F*kw#S195oxrv*vmrd!6^D0r`=+50k&ic^~```mx!Ck~^IPV64z{ z<`;oH;Cu)ABJ{k;DC|DxvrxaG7tF7qWWU3=(1l)<4xV$0p%Ftbnaq8?>Rd~TzHB}N zNJD@ zLwCsp+MWB>1G!s}Eyjes+=VgoUCe6cOH&zY54~-^4&|$s{f?gX{aopLavveb`4iXk zz8OWYwsRPyF!X^`^EoBtgAdIYFs%aTGpzPEX|u|C8Cw?myWC!GbtETkSv(UUWwjC| z9*ZZc0q2*PLMT^CzU+Jf5*PAXucNmMRcc~_1YM9&`As#79Kn!6xq~a8DggqdP%ei| zgz~NT(F`dR{zO%%#G;Nw3Z=FQ*ena;i(jfy+2WwILqa7?D$SO%{Yc9YD-#Uql}ov> za+%kwW`Os{LvySN7y-$aH)a8wYtif=+45JiTcyQRA(AbUtFE#bT}85mU(^ZJSm<@>{^c&>1Fk!>YG4bZ-;Zkm1fUk3|XMq4*S=z^`HY&Xq#U$5*F0ZZUE;PPSye06^~u>lo;}I}-Ja|V#V#$W6JVphfMq0eC*_D=nnRYwNYma{F@tWPR&)+i~#11X_K^6>)?+wf;HH`8k`VmTXh;h#jD@Nf$sj^Ts< zgj#rb3gF2!Yl!*)^#67)!E*u1I;nWG*7+V}2*sDW#Z<*vOVq{p;HmiPR3+l8&r|r@ zF8-0>zaAXywfO{J2g7nF#E$_7c6+AakJp}#PqlOavcOEp3~ynh672OHj(!i zr)LO=$jg9afwdqFdQ@uS<{(RiY= zB+Gu*nb(XD&HeY{0df@3eNz4=%SG*o4^9-t^lb3HJ0Mbt7amk48Hiy~6o2Rz^9n>( zG0hl?{dnrNx$F77$DnwGQ*t?bnN+BRPwl@+cjm*)5S7-`HPviNJ|C9~tafYj27&5PYRmjI4Ua{U|GJf&+UHoEljX$$Rh_9GUv`Z8 zEVm9X5U4soz+S0gGhs))q+3u4=?hnv^D04BzcP)FwfPy^On(I|+F>?c&&z`L7kzjo z_zglu`_CAx@tcHm>_QN<@kYY9mb@9TeA{Z0-Gn1j)EHANrUz?wSeznKRBzl0KSuOm0ceKd^h2cJv<%oJ%k(V)e`{U%h`wR26$YJ-yz&= zpM%oI`v|wnZyYo}z&YSGMPR+g2MKSngXMtvmKt0kF5|cl^Z5z(4OnQSTyNfC-;dpB z{5ST~XTQq%e3$UBeVP~W_Xv;Kf8reW5#D3piO4tKrr`|R^V`<;5gKcJU) zsr?^duExjt{9gM^_*sol@P^9+_G2J)qa-f&+k9Jb<9>RQ&lElcRnzz+9W28v{8Akd z-q6KWp~}?Yu+jLF+Ykfr@fQUrVNYj%kOvn&C;SWZEUZkj`r+3l+h;gF3>^NYIIFh9 zUc+yQ8#~Fl97_xTN=gcx1RRL)ugwcFeZRx;!oQJf9nMFXPxzoz+u=+F$-=*tYTeGe z*w66q#KFD6`4CJN{=FbSa87~V2)`+9-f$W>1NoyM2c4H2fV?Hh9~^FE_|N7)z$0%3 zxRc?x1+mS*UycO!j(G=)f@WY9s1$xzunsfO%O!ni?!{c;7yr2m*k8rx#zmu2@J#q` z=A9_J!VK`$=Hb6f+0|x%d>Q_yVDOpAnclMBP9V7Hk8@@?3% z$hh#Qe-4up&as%$egOA7q1(a}tbG^{{`6;|RKruG9sKF{NJgJvxKp_vyB<~^JG|?+ zxD$m^_L3R+8LUIN$l{7#HRUc}c!rd{hRa~2|4hN)c{gK&!vVqGFay^X0-Gfm{P1VM zZ{fMt%it6E;bV=!D#R21zzm!LB^|DmG6VM*x!Nizv#nX=sBpVAVH;S=3VaF2%J623 zoDj?jM1#P#@cV|wj-0^63Se8MESeKI0S&_2r0m+9!2LKAgpZZE+>jGEiEBSmu$yuM zThUp#*O~zRep^o9tB}WVN<8*Ea{`gMz((Yp@jwn3`e8`o^u4noOrECucq!RF4Wl>7 zyG{)IEW!`asmUoU!)Mcj0-J^Fs)0O20n4!pj{=u9?VAS#FMi5B49KHI9INo3knN`L zn^-^I0`(!0$BE!AP#dr#O+REcyannQBKwIfunJRP;HD>uEV2sE#KM}M=0R|YRmi&o zP0u=TEAg_ZKM?s55xgwwDs>(RXLa%F`9nL zS?{q5qu|J<|Fsn@&#?+uay|b}L3Mmy$zhlXOr*HwbwCVRmhCPLgp-h_I zB9ihHUOOGg+eF4Zgdxq0~XDb zBK&07&sf$VWoMXyN2xF8ORV4=+&XUtwm{0DyVpzvwn(B1SD3SK2O+$~V)7E|c?oti z+$7V0LMF$Bmr66J;OWJ{mI-z*ZlTg99VHkPavPW=+$`85P=a%TEw@In7^vrOQ9ZA) zIv^%c!F+#Bc%{^Y(k-I>S|u2iZU|MvEux~JbWfvVU2XZm9#FG))c`x%V$>FDcF{Iq ztgVws=^cFfZ^eVWXL#sp+Cx=`Hy#WdO{a-# zVN>v%z9?ZW!+M#T>CD&AC~wIm@IlkrR6jPqgVA&j-D$(-S1_8+uER$OA9th%?`IF z->s)=^2Lw7`DkodZBQ#q3)!bt!tRUCmPY5Z5v$$!NsV|LqtJI3it@?;zEwDl@8pv= z`{nb^e&DZ#s4<;y$+Okl_6qryJfD5;1UxTdeATbsyyxfJ_JYEXg(c+22Ky@3nL&7w z-N$}r5)Rupf}+yd4%`bPi_P}pKm|gZ;wJJ7cS&# zpOg366)t)f{l99z){f^(Wc!OcAcTcsk$Q91!J^4fxrI#%4IC_by%{LKgTR+v9V~jh z66jKe&Nx`~yA42>DYW!p(Va&CJyM}12aD(p6&|I~;)6xYz*~j9*M_eA2i2SXmMgUA zVA0|{l&(-{;lZNKTY;`r==6g{RteD63N1KTR0R*LuvMY?2aDzqZByv9gGHBB13gAi z%XhG-4FXxXR+Ub{thj`A3Y~ngsE(D_EA+5~_7+m(SnkLlz_w&3ug!Nw*6@cAmfgfx zPS@}q)yr-ca0nHT^iBEW_x>5~)E8(bIzJf~XpYPVkfqyFNPyK?_ze3kH%&F$w z$@vpKlS?O0HK*nIpuwy{!!!BtiAPLq!>l}WjENJEL^*avgEn55J_S#)CjMb9yfoZ1 zdE!JiGJ)EMrF#gY+60Z}w&fFb-2RayHqEwtwwN^AXv?36!ow;6z+C5;neL`kp}QF% zvajAes#wjfvbZgu6K?ijzM_~tOwENQQM0zNh-nkCvT1oN!6i?3JuE3W_jJ?1n*>mT z_h4tYDa=A4NJUxgfcnu6ea@Hp7JmtySpJLn0-X}K0Fl~ElcS247UPooZ0y|3dLw(< zd>&AbDvK9Mwf{HU;1b($i9J#81U3d+Fn*T~%k0u*wI@@io1{8P>dTc~nhLoxrcF00 zv7&sIvaFyeYwMR^B4!K6i`gQeT31lE7H9aM=#ngf0@|1nlyH{(G-Vnop*fsFbHw;b zEEV2bdgwz`{Qp&hQ8q+mvIR!(0XBy0JnduqgopVqw-#hG^z@AI6^4Z;Mk0vmX!tmqVohB`{n3V+|4@Ji#%%nW07 zW@H@oGJWEFqq`AWj9Y1&?hQJVLnDlw)?Qfnnk*EhYjvNjjM8;kC`#8my6sfB(i^DR zHkn(CT+P-lnhmWsx+-qdL=@x4v=(WVw?zQd*h;`Kwy~W0>e#Hl#8!7?rloA!c51rg z^ixn#Jv~0V8Yg_hc&1Q_@5J;&@PT^t8Pqi*>F1!DdcH$F%GRaPC2&*?qcdGuYfsL* z>(kxP|Cy6Ow!`UUd=DB!*BJ#k$AZ;L$%LBAFC*XX5`!N z8sXw>BV00G50>g-u$^4MjBNiA4k%7UbU}q50(uZu#hCa>UU4-w8g%`~+1dVMS*8uX zo|b6=i<#|W&($Shc{rfi&LkaB*WbpVRoTuYUC{I)uO^!fsj6zpt)c9vWFg~o5F5)V z!>4KfZSrCGGyxmL>+9zY^^Vo`rBcIn(UDYN9e$3uZZO_GG7y`WOmrFZMv{qn{X<;? zBi*rio&Blg_$SHT`oTbd=e*vou6bkOC6OhI>JqW;zG$j$pnqs&Y~G^!aQ$MplX2}0 z3^A`Oo`@L+W)a~mBAi78vxs!|4@DEZBVF+!yzVv?OC;;NIOWvt;aIZ14+~0zIJYNL z-QBSsJVsCebLo!*1nuw`zq^)!^(uR!+}9jlM0}q2ZLeo^ zzsE^gxgho#7{W5b`1~IuzuqB5-2dKQ%o~cO`tfhv2F;5m2kUxcL$O5vA-o`4EBhBu z&Knu(AIt0|H#IgI!|&aVR}v0V(wgg&scst&HJ8yHx!2p9rj_{ozB9MOHWyDJ+Vjt&gOyE4UiS#buz4>e_w@!iPgp%BVZPirXF zmFkak*?J_Y646_lsTxhi2m3jqUQwbmm560cP?KMmlEOk$7f&U)sp&yDD~h^X+&35- z%%CTY#745ZjwTY(-I=xn*xd})7ftplY`9BkiXG$960zRcSf*OCZ!ndO4n#XMg%pkq zmWUXYhAe$OxcsFsdI> z4pE3;-OhB+sez=PJ04M0q6Hv5LU*P(-j&o-7?+rqlx}#r{bsVifI`f|1c4Y zq~$HUB-xedA0`{5l~IN9Puau-_D zC=D0zF9N&y=I=ZwALDU?p3(Y^p8Qul!w;?PyS#S+4UhLBug7Nx?Ya*uJSV5_-X8hw z+3|Ddc?MI~1lXPeC^gIYkN-(G;J;~y(ry3&*PdkdF6|Ov`_o!q!DPlPPaq3~Q=qkt zXm#x#zyy4e`V>KUxx&P&~9O51vqBWJi zR01n1`M=_{#1e@iDrfCxq$N`0|MFq}<-`2ThcOJC$sr`Tqwpzq=)dU9q&>1jt$H6% z_a86)_@PfKW}Cc^H~M(&KpN3DKIhuTCoHpY@#9#2 zT!RnGs>miIFd^^*mneg_QC;3q147=Cv-~Ja|^=6fNuV>QRui3NP)|_FF+RNJP?QjDg zJcWM256y1~ycF=0W(VN=Pa*u2xqsOaM*_X~loZfs%%uXe=vng(!1tVTjKHrfTh_*U zcS^J8q@IGGH|=RCxI-$wAO+t(r3Z~(GEet>ziQ_nvFx)CAG`gu?Z**Zpg_-S zxX^pesU6bmD_#uPqX)oU6w~|=Zlh(qgU^roLgV5V|4At9zv%;y>)wsUjB||TP%$p# zGOo?1$5-N=pG{_>WrxJjRG8$D5SsQlP&MswpoZ%?OJxa@ph8xdL}PMHChYpus4pQC zIN#bdtFVW5fyevS>0?*A^kW`$mkx928vk`~e7x(ePi@LY(0v^m*U(;h+9%NQo3u}$ zn`wIK(sW$I#of4uuI>8ty4XE4>ryw6p-~;k0Okf6;Fn|t8Gz{^10V@9@UI(W0IY%x z03^u3zY<#jpn?nlbdW(qI>-P_g#rNRAcL+TK?Xi`gA9OGkO6=M8SqyH835=Y10Wq_ zfT7Sq20YVI1VB2-pdg;>0J%X1fI7$kC>>+~On#DB0%+;lC9s>YSth{yaRZ^aOrP-? zS>`c2(zbaR2h0?KuAvluVI$JEA+ow{ZHv(t!x^wInuw(0IxsC|$>?Y-!XLvZw;;tN;p5bpke;vh9wIY2CFuqT5B|=p4ZJ^QXQ-t zT(Srssga@BG95ey8clTdEkk~wj)lGQ|At`Qh6TYoY}~Tmp^>JhU|mmp+e&=K#=2s| zj5sEN2;bKs>YGB0C7wX_1gBkBHylkw2ZPDvaGm1!IuaRK(!T!QJ|ogKlEATjU}SK} z_&;Phm;(7PBp)J43}Zy%1KkmPfr0N!j1X9|D3XGj)C41w;JKs}z$^ptU5HrYm`IwB zsf`)|trsppBemtyDfU_IF6z0Qq&9tIXe1fyULen9)5#a}67hIyUiScI-aj-ZWn>d_ zA~`jiNjJ+B48@Y^X$o&f@H1ocFb@0yV(GT%2;@zo{@n8EPeg_$hPHcx3(DEbP@!E^s%AQ z)i*M<6U#{@qC-h64UEnl8huHv9V#SBi3zDkRgp=I<%F=AZ^M`Wo6ncb~4O2&L*R&{v;R! zHZIcL58a#U>eHm@AL{PM5>hKStu+vik6>@3jYH8vvN8FaET8O)?uM4Dt6>qXWHhn83cl1qSptGsfMUM4_(HMH46|q!}Fh&OC2~lZKN!N-~2m5%G>-K6^p0UL!fu`B@MP+&k1wS%%i6 zVFr6bX28%oaUi|WR4U;i*BhPj_&_W=BnoRuWCVK_5y?kqNNNawh_q+iLaa9qyQ?-A z>DgZcm#ah7eyDsjGz3Nvy4H*3`w!%HISZ*4naSH0n^lyjJI$`FXSJYfMm26 z(haUwLr80-Bx6uQWH!u%dL^=`0mhC=^URzLu$OWtb`K)UxKqi3R7%FcC5ecz1LsEh zSBZ;AZ#)j7gE=YB8o&?Z##8-J0+>b&^h+B_m&Qp^fLtGhN?MW{MuG-yFgk=)K zlO|XvZ55Y5rHvm82PV3UKXva;Hq{4t?Sp-nipq;eJ^VyD)Zs{{o}@bh)t2CalXScFeV9b#v5gM+rY%$1}#Uz8A zDCPl0GAPs29<175jiiip!Yyft;9xUpR7R3W%xa;4y>`We^j7o{UKR%yP80O2rjAfH zDGcFSJru(+CMC1TuJ(;lG7FOWu(;_4M2PwqtVd|I@jts@ST}}OIr64^$ zych+KClKsT650d%HW2B@Z2=}-(`s>3%eD<`T2|41q^Q?msvt(`I@*#%)-PYzvf0)C z^beG^qjwq4SSYxVn4Qv}N$9?qXxB71y6h_L<&1(!iHaM99Z)tyX&(4)#Gxl$xg3gT z%$^XI@+kBihJuO=z&5&c$$F5jFCcB`jWz@3^8eno)mCBLEecoz+6a0`yCSjScvl~U zf(lRA8v+QcE_0Nv8eQz-atR7jlM)byTU0CvH4J2(H=5Rngj6bXpxm0)}@=%S;qaP zj4$6guoI^~u)dpTj>O_UbOFE@;FCdkeq?>^kL{+t!4Y@>Kb@!sRfiU1cmzAx4e{%T z&cjGGJMuLS26Y{4Vk$ng(YK+8ryk*n!w)I;!8=bpTm@*hP)<23h zCPm3nnF+(~goTI>phQ$6HBwJMh_1};F1SbWT}JQ8{lj#e#N!aF&E0cFo#$$?5}LK+ zLj${wU7RAdJwsTqd&%g4c8>?*;`+%^fP6`#2yM}gZAja=Gz*AtsH`QoLT!GEXAm_2 z&MjoIlu(B>r^M4W=c^>BX#G#GzSlNpql+6GKodoj&&YADhk^9O84dQ44u1>EFWYhO~@DxPtf-4s3jCb#L zxj<|utS@W}&5<%OO50NvMuaAGVZapbAxhl!4J)^_Y>8ms+twcq%JRhTDp7E3mNOQi zivt!zxfa+Md{{EO5#0&rKRs94UJxm*a6h#K=K*m}Wg%jLapnQz(y2)wu~HI((rzs7 zks(wod_xIRj(7SRmjRYdf#sk;7%8E^jU@VI_$>PA)zjjMe6@6JX<5Iy4ZJT#1BX?( z+L`*Wq-2$h>XrVIxMqaOrqzB1rx4K9RCYD9_N>z@ho%bxVv2?}29y2t9#t@^D<0oT z6#OV_1V=y(E{tqyS-D};DoVyl1_DA_8K{khdXbw=uB+yvpfLGW7}Z$Sws}jWeFMZ> zENW(IX~EIPlY~>8G9+q%@{!>7yBc*&9d?q~^-nw1xJrmzL8(QbJ#qA^)=0;Q2V&(> zrR#8QsDq;@HKym6%12XmHjM3KayAC&5I^^MYr zh$3|J#2w!aohIzRWqW%Ijy{pGoiJ~7KFG>R$n{`ULNjul=oZ_lNbOov9-O0tm@OC* zigW-Tj~r9t5?kVul=NjwB6Ly|{59A=up13Qx-^?>$CU0f98*Ml!CbKKAUUXkIGY44 zGM&O9LChj8)NPwLZ-w|qvUWIQAL7GNkO>UjgFPT;D$|aWgo<=PP}9c-gMl&$)#IoR zqk$W@G@iJUh<0+ywbiI{}QIk=S}D9)PXRZEdWPv%%9M;fUjHXiJOu@1pi)CxNQ zmVwLR>hW}N307S7=hyo-9bO0KvPqK0?LQv-bIFZQl)yK7*n1RVW5@I1SUk{ z!G|%hZ;aj@<^^la^Le)f^XOLhFgynDfq@)@aYCgMxaIIsRA-2Ap(V~Ga2@+kf{oVP zz)%#nBpVxFc9N@}C|)A@xS!7B44Dt~6NSP(T(=5309r?DdC|@k6Yeu(R&)+ArG*Tl z3C$UU0^J6YwC3Rv(@TlmtDU3_BM{h+xZe;q3hNa^#6_Wkm`trVyJMXra4tH*ZUdMZ zZeY773m9sgL=h(4yk$Aw&e$7|?uzav*@jV<2SiSufypce#UdDg5#B|ZMTaHS@kEC!1v?db$=iHa10~G&@ua`mEYN zJi0`bejm^LazX&%=_6}Dn1!@pN-xnzz>GzaaK>@Ew%8Q}`)DaUC62|zg9M>@_=v1) z+0wdU6{Zd6R6DoSquNvNL@a>`1RQlo`{Rf;YDz1f=1_8_o1p?p3t)Fd!@5CBx)%&s zA^MgC11Ph!b$#2mmQ91%vUO##G}39lpb=vY^IKxXS;I{ zbzb*}-ek( z)V@?+JR*pztV|=jVpC<%xL%x))TEfoP|uB>{X?VLrzhVbxDLe=*%d?fE$g&#Nc0T* zz-4sPScv*$E2c8}g^~1Wf=MNMc&=ALt(jDTIQ>X%q1tJ!hj7A>n+_6#1IsWB2bPla zT?(h`p`<}yF2T?arHmOo0Z^0*@)bB;s?3V3 zACw~?{uZ2lE=ltwF?E`ULP>8usm3pZNEn^^kf}R}W)zF1a_2#wnGU*7!gdK}mY{yd z#Nk4?h%pceci_MR))78YlaRx8#vh}l5-aKk^29Jvj^WYa0LuMN>uAY5f&Q_vXjZTt z7GqZz=|vr^<+F=1dr*kYA(AZa0@;bGOB3itS6#VWNfsOmbqZOrb@l3&O<>wg(hXj0 z7Vn5Np~8{Tft0Ynl36g1A_&6;qPDV_8g^82Q|So1vizI3Y}kYhk_080V3HXlW%P01 zKcj0t&9BBJq3?#0T)lLp^dQgh5VAwLh1o|K4eaTssC6;~BoQ%V_=7YtAQh)4y4{sp zUV6b~>CbBi_G76UCI33qskJ@NgYX)0aEDWrQT_Vz2C^*TNebC@T1R9CsZTB2peznK z1Jo4YG{`E|Znk#T7GLy-jtfDi#fd*@1ZG@0EV%9$M{^apG`eALG+#6HrBp$7TnZG{ z{X!?g;UoL1=!V!h&R3i$R}TQ?T?{Q`SOPb8RjX@nGeIUdinMam_Vz6sA}t+}Rm-<5 zM~*w~12C7zI1Dl&+yvR$v=P%M0a`048>XUIN9Qy8hoElY`5}ab5Wnzi55lPA2dO6C zFG5QYU9#eG^xdk`xpaXed` z%7OSn#nPNB$0HKyh0;j}`RKq(TvsNja*A#1S7Ts=up{gn?f|(4IcnK#eatlyPzH)y zv;rGv=s8b1X!Izdb8)jE8(Uzsu3NrRJTjc@=8G{xGyM;#rJ=7%SFC)@?vo zfu|rIJite^a=5jEjmN}cFWcIeBP<1Vx>}CE!gyNG%aYQ9kElqOJByxf#r;Ap?J*yE zoTU|JXiVu^L~!Bhr$;5BvT`{p!JwQMIJC$ibwg6MDH7dF7hP#6EI^B#>$i)H7R>CLB7s{*xj;ruq z+w#R)%?)#%VBulz13ZBU%_t1bxb2H`WRH(psdNhy@*3BJTi@Z2ja(9Ey57(zWd4@?7u?F>LzCO!~&47n9 zgyTh*R(a_S$_)tayOCopJ;~j7GLmTTq=9jzo}*3$(+hznat z>**Acy1jsXgji-BSj4`-%cE10%nBbMZ=;UyB!{pG{G4G3gHB09VkZHxUGa5p83W(ZOa;oB!46eeEM$7u6+t#;;rN{JWqs4<5Mo0l_ zfNCze#DGZgf{_H8Rd$Z5w7*B0YA#d^yE-#uV%Bo`g^SkbZ{ag0k8r(?tV4f}3Vr;= zDY@qY_C`8ET^OU`(qgG)mMc@YgtVI=w=A*_3|T5cETjmE?5JEd5Rrq%&Pr!7@~gD~ zk8Wf%=zveTpvWCah(1$9$FyW^Ab`Z2WIC7w^ZZyNgJvt++FM&TDK`z?es^qCobq8t zQ1q&Bxr}5T7(^UktPAZx;E^Hd4RwQvA?FyQt@4 zh{N()QV!)jI+?Lwvo#!!m;iX@m8cY*FON69#B4@Hop=ZQ*RRyQrezgg|f@7f2rs#H;!^H)iq2 zI0Djp17niuc%#fMqqZ3r_m_xlcH#@ytomVgMe)kfPPxo0*0pPwS@H$0N19ZQ54ILl zX7)m+&I?#6oqmuQVOkF|{{p=s*AT^p!P$p~LMsWrW&`R02TBn<3UJmAX$cwsP=`!0 zp&@J{Q1EcJ%zc|Vg$q?>660SnOLGb4Tjtq(GYVl z8c4Yjj2@^n39@?X9x+b@=oA|Rqo;jdWgfFQX58IZxypfKG*8fI#sd*ep75krG)N_> zm+0Uimgwrvl6HF1Xn|YXR9?y{QreH)2YSz6+#ubPFATI^oDp7s=&{tfUjoZ~f z&@IWnNfB#9%j2`CtqAwa4~{16x%<;;Pc6vVWwWXzedq77f7Stl|9 zO|I@)Xk{@3L%<^d922^*5)uI8${885wh+g`IONTZJGg_OVQ4h`ZUNvnBM>3^vtAS^D1!s~T1 z?XnmvbC6JJA`>VbZ}t&m7^@tkbTlNC9QHY52?T48!i`vtX5imnUrOKh-u+@p(OQ6y-v2*o>6&0t^nMni{^_??!uugR@QR%*u zBJ{|`e`jhyhvB+$wY<-Wr;sEUUP!MsEuJdJ#q%U|M63(HR^s|XYH3ENY=YAI$Vy=5 zyvIRVLL^c}YQ@s4{GTSvp}vZ2ks3-V9>qzNPlR?={EA&?-Bgj-8Xoa%m>sKwNp|nq zQN@yJGEoo}naNVO5wT?4Nd53*Gjo|!zR5z;!mUCSMcwtJICT;%Op(EC(}^>Iv&)r?kgeJUzTzNTnWx^>>|AeP2TpHQLF06vE%z z+BUbXSlgnlzo;l4?s**6Q7*UNtkh%r-iP?*pragU41ETH4#O9t3?y>Ex$CgMTAoBB zFJF7~hD|i$?rp=2izq=!(Ok+OWmF2L7lxnT=)ko=eJQLj%G;mH_TdyHF|FiKH?4;Z zksbN^!~~f5fN&n#x|16cn^YQ6#|*M0JI&ai^h}*VRY;h-l)hf8&#DLE1r_-`OSOy#km&duS?s=Id zD$$_D1#t%ckWDf*f^emrKi!jgI>Z1|fcuX6_#$_g7*e2)luKGLg7oB}-)Y%+RYk>f zBsb9&RdHraYDi2ol$`rbJDuI>R_uYVO1T;piS@zV<4lPAEk0!D@g*c~0kpDGHjz}M@GFLc)32hMDoBO7%Xiw*F+(t9 zFk~w-;|(LoExep@eWjW_$2S}q$aSqMw@Hzf5JVI6m&hZf&{!jP{NeVBvVsT0#0C75 zv|f^O32|tZ$nQIn>v=+@B$5b9jL3)XMQ2>()EDkt+R8~ph2GRxx@3!ahYZR}dV$Ku z{IdKUWo8gUP~V6^A>~)EnR5`On27F@=m?IujMaf(=wV|4NFm5g71=7r$+aubdAi0O z5fOMV2-EhlzwAr%2-VX6Re4n2A=;JpBVn!JS3uw-+L|GNMU=)${%&3qZg*tKhy zd}EI~4{2*8ClQdv#a6kaA%_ZPHJOUQlnuV*249LiSrJ}TH||Wg9F1!a!piQ~LlA<& zEfDS+F4f?agq#B=4JU>?f<+~TPsDR#{^^kK8qu2*FHWRD>nk!1Gr4GtysExHDt9@B zQX|PF2TR)dwz{oQwny+JA@0!2?jFHKSG26faBR|Z2D(SrSV8*n(|>*LAjloj?T`8ROkI% zYcU$#U}m+a9sv1=0TGJQ8j8m(@>%=rmVoO|dSE#)3EQM}+7*17XLC-IGrX>(BzZ#@ zrme_5?I1-_vHdvUixvA;=@79FV+uBU-4)F?A|!9HWdd=^bHBc`@1<5ova@EkG6-jxBi;%Ethg*K0Kgn(71LL=HEVEANQdXnk0IEAsxL!l~x55gHpu zX5zF37;LUKmTFcdwiMS(vAD|XrRo68r+`p&6o*ye#yzEFcItEex>mJfAa5P*c5q6k z+rg@_wu)pK%q$S?XzFCROkm={9#j~9Valv-$~<`R0Ez=o8woiRpgZBQLG>!U-s0RW zW-zm0U<8n!9N{WiiL7uN(8&Go39A6;zrZ!VaHJ+p^2VA;4g*GCNV^z{HPWvW+}z2>Y>uJMKdmnk6}yb#!}Vb|^rSHaxlI z4WCuF$kYW5>ahld=5&oTOlynfsXM2+i9H`%Fdor{tWQGOTQ^TavxhLQP;t3)_0R%l zaMQF^7o(62T%X(Eg2A26%kJK##i{8B^qZncv71R&t;N`|I{CE~rd{>Xs<3m)ZKliw z(Eq?JvfDT-m~{2+Y)ENi{DTkXs-YENmB%3v>uSCt+JS0}(<2WPr;27#+ooQ}qh0Y=XLnd?oLsdtduMK~Fn=+b%atT>~kG0o_#(DcDW zChGDu3%*D19^=dtsvkv&70=eGrfwWMNcRX+v|v8u#FvF)Y(~RXhH5((1XU}qYowvL znQMI2>&BEcp7pS^znJOVKwFtP$WuLKZq-Y z%3?*g3gtUzuv@wF`k2CBaCv|HB0e4NtvKa>ORL=?*t^$Pk_ zQZXjCEI2tQ8ke&&7?)UsMRR}^B=peRNVICo>S0)v*Dh7*gxj4=-GtCC-#B3_jXeHb z`q=>!IfvqQo?;)so7$&Zh+MyL$&N~fgs4{EI$wUR+zSlztuI_8`IRY03kJj5RnZf6 z!6C?Lv+TdaozSl>%XPdo%e1lJ;k@CTf7hFNb2|#M;Hqvc? z(P*S4D~M)n0Usx3O{&qd8kQ}J0OM=;_CudK9L&SHF3fHabv)6HMCrk4r(n29#tdfF z>EMj~#QMS0-V=@)VyYc4Y`MbvRWLToy(H|uW+KJF2OlRvwPx3Qi3Xi zy92p`#O-KVUG0uf;Lz##)(LpU!mF5|U3XZ>dhfWBIg&>=v><}|6-S-TzzGu$?VD!6 z$GV)ib5P|$Nwahge*_qN6V~N<`kUA^dOL0ofa1)4>5x@qfRbp!{Krv%c?gCDm_UxhUDK~%dH|Km zNma;zx9L}+!So?;GVjDX5!=nPW`Tx?fhaNeNX&{@9@seKYLn&QR=JKa#io@_Em%Kb ziw<+#gAmnrgGF&)5$ARs5`(>j+-+7L$TROW2o&y}gt=1K^o)jh0X{nk94@AM`wM6#`geBXdWx?oac1>4yh z+cGhJH+VSiNy3)6mpV+^nXOBet8`W|ouLxYL+G^4qw6Ive|aT$Cr4;}r=$qSWelAY zFoogjrdeK;i48dsH+;?9K-uV(r$KvLUB%S4_=23w)2+R{vz?IEoh26ES9!qEtseHe^j{ zhOV9HwP~8mIX_B=b8oP&Q6nw9QFfst!;fP>`jhNRk))$vVVBZ@0#0MDt;K*v$KnLR zzFk?gA+&}aeO6qk ziI}teECsI%Ob58rgVqf{c`!5A=(Z2-yydqb(YQ#?*u$esP<3{JRj>xzxd*4fP17tc zMKeAnsR>~e08400%xE@@@0fng_E#A6N|HKDb9WTrejJ71`S;dN+(`u2H>s7)?Db1*!ncR zLMi(Fj+At`?+%(cx`jq$-k9b3Dzx0vaZ)?jT3;v{KUqG0&=Fa2Hl(>dtZjuv3~kHQ zq8R>RfWutp#tw8Ns&Bh2^&@ebHqo3m%R?GcrW`$#jc=i4W-FQ9_rYB*+opg##Rmr2 zJLoccl3>5gW_KhAalmoGmjHX1b8tK_sK6OKkcL1cegQakI`&UvareXDh=wdM5e(xR zRedCieL0)9Y`m*?>jX5U4UmVt(Q3n{jlIsT>$_t`8|JSVnCTwhNa%2FK8|)cd|;IO z0y&Y+)-DV)#9#$F4FSNmJcY=KL#@<+W4L&QVr*V5Cl}Y2Ay~H=& zkW~#g6D#IemzP23n6{y7I){zDBSk?gWmH;lO5+wlOOdzG)RvR29HSJ zuV1y=QnK$nVnpIIk#v60mmgy#jTyyvtyF_?i-1=4|j0fin&`YIJ$kG zbq2H1!_&6S!!K7rg5CfB;0)i20cVgOyajT9j*s05*PgNHONi0i@D+*33f<=o5mT`V zr`*_f2hzS7@9}E$q~A+Ftk9No%k1m3W@hi6MH%l?68Q24;*KB*;YJ1M@a=O=`? z(ZQugg?DUS#OiQ@z}_(yc2Eg(CvMq_;~16$v*ijSTAX*I)mo}MHV!*F%-Q7z%k606 zqWTWm<}NPSf|=y1WK)_I9__>dhZFR;8FHrNa#-#K)UeqkFcb0UCMRr@r*h14MTGQ+ zD8FD=XV!Tu*ZZy;yyq)H`)X=w;EvmX%O%EjEKrwbatq#SBiRhl5b!n%DA01dK;Gao zG2F6c0!L$DVr=}5QW6|V*v#QVM$Q?=gSpe}30Fi@6sb2k7R-u`#q$2?Jw=wC@v+L_ zr}XEN6}JaS@DSS<`>j|N!h%p{l{y1+rD%FGQ}Imv zzMInQZ|v31>Q(Wh^SFLrBIOrTb!QawF3F9Jb%kB@FVP&U_t>*2s+4=v39^<`@SnAE za9RuvZX@I4lsq%CHx_8dYaC)ORiHwas$G@r(Y-mVT;>=!>u;&gNiN!r9a$Rma7h~a z*o;IAk(4`4UfOwO5i<*H7?3dNJcuC5+I{!ySbmOLZG6n)FbI^!9C=dHNlS~Wg5@Zl zVrg#Lyj3$WwVG-!O zAY7Y_m)4Q?#XbI88qh$R6nINUbp>w7sBHqf8PyfIE2Fjvd@!TB0zZ~f+XOz5QC)%0 zWYo5R8iC3?kR}9<1k&{C{RUqLOq=*2nDLtPN=7+3qpaagA>O4 z9Pne;{HcuxQdgx@g90URhtVc{lymlP)0|4DHU*);<3Tu>W>?OsaB5Z12|QWQ{p4K0 zIXE5Aw;}|02pq-7a`abbC~E` zhrfuN2GTZxuLWu_eRN<>56XzyZfmL7wzh1yHM0F;r2VCwZC4fhp8_TDl4vCK)?;&e zxF$DdSA|mE=)g!M2g*>DTbw?cR$q46l~*mB6G~OYtAThWlrp%GwT^efDbrGlLaFJ? z(Q=6bPz$K~^U}&W!RLc!VLC=WTGnVrB{mcQ7)#)jfpQLvhejZ3@Qc7lf@mP!C-AXA zIj`wSIe;+UH-e$SfU5*Pno&K0O3-;qPet;*h?0YM1vHY=n%WrP{sI+H4z3DF!+jv# z+sy0UOdfz@4aGD2NAT@|1HTE}S|SsCPbTXKJepD6W*HjHD=G9`jx{Op*8_DyI{Nq{ zb9(xxjHURAha4z5QKRz({#_8EMgqTDN^4?HPs*WXs5O({rsQAE$xjJXg5LRhDw3bd z#SY+w2>&{#)lsZ}EKmV8g81e#K1?_%a5#wGdZdK3N%PahsYRz2=@DPWw%-Dxx1Cy~ zXC*#Abx9syC`{L8a-gS9;S*QYpXincdLVVG1SyF`NwxHVNLJ)j8m+i8UfqyMCIvp2 zQQHMR6sYzlQd;YCMBKjQzdac>C9s@Ub3W3L*4RM<2;801>Izg^EzP8M(}13zPA%%3 zTC_6~Q3}yNdQ^h*6KI>=qeMlHwil?FZ&4Jrz(9JNjiKyRhnBQ88!iuq1YZl>kx^3u zcLu7xA6LZN@f@)uu$NI?fl8~TP=|v0sT^xk;O_>iy#zlWZ=cT*JHHv=X9`qwVGbUP zS5HQGA?Ef4YpywZ!-54GN166)(A+)GQD`rLFANIR75JGzd1>|Zt1)a_M&9qt_cT5m zAsR>E?F*EAV=#U&3iDzv!F|oV?(^uy{{=yPVUE=i_#+v$UD^IY&VKuw0p7e&CE7k4 z;O1Pl?b`w@YP-G2+oAg-q1l{vM`X88FB4o_wz-SbeWMb%I zuuO?H)sHQm^@XU(s}Z&c$(N%nuY{-=Oi!PERJkY%L)QEBpCjgfWg)Y@9&aEu2J3et z)|(N=6sO1U2YNwz7&GOmMS7kWnJhFsW?ev9qJ(`Oz%wWwZ-(yMwM1|!)^a~&GP z`@Qk%!v%@JZbsd!JUhi05U56kqF>K`buI1rsmDqy{m0|=0-~X&>G_Gk2YD~@+!JBw z7<$|vXiEhxr)QN*sn-j4Pvmkqm*QU2UG%J$UzC7<`Wza*o`Tv(lr1grxk&tt2nW)* zzxEamcSpR7z-dK6gjG@~yg;Q|jZjZsEi3AIX-$J)>NMb&FSqfF=v3rt z&KX-&c2?{kgz7LkVS5+B(`N%i8!+>w(4 zm3Jdfy|5CG5`!&#EfPsgSSb3XQjl~676~^rjggwBaVAukPRxFfM|e8Ivr*62BdnJ~ zbQ|LRx)Av)LY@Bk7=*==?Zv%G-p-4+Jz(7$Ml9< zz=F{_0xFYIbMauBm;SvKKbY={5nWDd5c@6~xmEo9^uLSC7>5Vb zg0utQu&@6H)57#pD}GVxyt7W7(jY!?KXgeO{=&4b75|R3^3pc?i_(#++VJm8kGJA4 zPHzmg(Jx7z547PgNvE%EMi|6K=L@a)OVdlO_;;lAYS`=hH^tjN1#N0`~;|b+8bBS~q+=@awK{{4E}&X9IsS@L!La}9^fxUJ8Pd|GD`Xg@IwKYgXe&65pC(kNas-OgghCMzG(E# z@|-jJa>@?>WuNrFW%Nxs`EPyF8HA?SR?els*I3*#UeSmC#yeek_L=|5=rccini znfYQ={RHq!!H1HE=K}u>@IwKYgI|esPR6*nDPDfo@MTJe|1{`_BHw3%Im{KKNJq;Qs~qp=L7ad>=fmV=8kw+O@#9 z^~=q`FE0}Y{L@C?)LVA+N$0UXc=#+H!S!H!_Q1)i*Sl?WytkPj6z#!v9A~uo*&}#B zv-36UAySimRKzwY7XIhi|Vrck>_w zua|dc^JZGmhWov8N3(EAtEOG^L~JHocOYz1*!rP~mQQ)}P58a>SVr?@cByT?ig+Hd zsd~K)xUY4rHwFj$Ua*4TnOS{aIQ4G18^+`M&U{pQ}*4I8>RJZPsW zdpy?l*5AAD;VK}UAP`d_J#yKE9U^3j&5@j`*!Mqi){95Q>Yp<1?Y8w5J!-?bwh|RR z$x~uDq+rW2Ji@hi-MWpNH;s(;Ru8Wp#!+@;#d8q2WFCI0I1FKT_)PD`y@2qbE@-dw z7M7PaqanHSceacjD$bQF@BTl&H4c)u#SZ_RJ+9(dT(S_Dn z@U0mzr&2AY`QZ0@)5md75PFo?DOcOiWER-TzNv$b$9*S|rC3}SWNY&4UG*0&BL9a% zafL)<3Dhe4X??Enn!qc-bXECr-u~YC^9L1Q>v)BGgBogv{iJuqW1?3Iw9%Zu*82+g zMurTShvI96xDGg`t^8#dQ1}sL3>yBZzn6cv;^BO${IVY?yf28AjPg-P{rUT)__8x7 z>_mK)pLwhNflf03R5#*ek5G6zGE)AEulgT|_#=#rpX?S2UoOR`siPkMIB+aiER(W* zV#PY;wcm#D2{X*g^$+a%U+^zgbU}=Pw|BMB!F`Mva2Ypn}h1* z|JjJYIuexqMd3bmbi7ylF;D)eUlSJD8$)=&PvML|=KuBlPvcEFzU(;)XOwWHp!}8K zZveO$f8r0>eNJEN6m|Og^Y4LdNh!YULod9|_%f)kuXSBoY2@i-t^bA` zo&SFm5YtlmwJtds45@M|g7Ve#w*fH!$WCb30~}nDF%GCY3cm-m;4hbwp%fd)c|B_>02?4qsSlS-Mf0}>zx-I|X&wKpmu%0ac>iPd4&Paf_ literal 0 HcmV?d00001 diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index 4bd5847..c0acdf2 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -10,12 +10,20 @@ server { location ~ ^/secure-no-redirect/ { auth_jwt_enabled on; auth_jwt_redirect off; - alias /usr/share/nginx/secure/; + root /usr/share/nginx; + index index.html index.htm; } location ~ ^/secure/ { auth_jwt_enabled on; - root /usr/share/nginx; + auth_jwt_validation_type COOKIE=rampartjwt; + root /usr/share/nginx; + index index.html index.htm; + } + + location ~ ^/secure-auth-header/ { + auth_jwt_enabled on; + root /usr/share/nginx; index index.html index.htm; } diff --git a/src/ngx_http_auth_jwt_binary_converters.c b/src/ngx_http_auth_jwt_binary_converters.c new file mode 100644 index 0000000..8aea970 --- /dev/null +++ b/src/ngx_http_auth_jwt_binary_converters.c @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/TeslaGov/ngx-http-auth-jwt-module + */ + +#include "ngx_http_auth_jwt_binary_converters.h" + +#include + +int hex_char_to_binary( char ch, char* ret ) +{ + ch = tolower( ch ); + if( isdigit( ch ) ) + *ret = ch - '0'; + else if( ch >= 'a' && ch <= 'f' ) + *ret = ( ch - 'a' ) + 10; + else if( ch >= 'A' && ch <= 'F' ) + *ret = ( ch - 'A' ) + 10; + else + return *ret = 0; + return 1; +} + +int hex_to_binary( const char* str, u_char* buf, int len ) +{ + u_char + *cpy = buf; + char + low, + high; + int + odd = len % 2; + + if (odd) { + return -1; + } + + for (int i = 0; i < len; i += 2) { + hex_char_to_binary( *(str + i), &high ); + hex_char_to_binary( *(str + i + 1 ), &low ); + + *cpy++ = low | (high << 4); + } + return 0; +} \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_binary_converters.h b/src/ngx_http_auth_jwt_binary_converters.h new file mode 100644 index 0000000..6709ab1 --- /dev/null +++ b/src/ngx_http_auth_jwt_binary_converters.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/TeslaGov/ngx-http-auth-jwt-module + */ + +#ifndef _NGX_HTTP_AUTH_JWT_BINARY_CONVERTERS_H +#define _NGX_HTTP_AUTH_JWT_BINARY_CONVERTERS_H + +#include + +int hex_char_to_binary( char ch, char* ret ); +int hex_to_binary( const char* str, u_char* buf, int len ); + +#endif /* _NGX_HTTP_AUTH_JWT_BINARY_CONVERTERS_H */ \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_header_processing.c b/src/ngx_http_auth_jwt_header_processing.c new file mode 100644 index 0000000..e368c84 --- /dev/null +++ b/src/ngx_http_auth_jwt_header_processing.c @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/TeslaGov/ngx-http-auth-jwt-module + */ + +#include +#include + +#include "ngx_http_auth_jwt_header_processing.h" + +/** + * Sample code from nginx. + * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/?highlight=http%20settings + */ +ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) +{ + ngx_list_part_t *part; + ngx_table_elt_t *h; + ngx_uint_t i; + + // Get the first part of the list. There is usual only one part. + part = &r->headers_in.headers.part; + h = part->elts; + + // Headers list array may consist of more than one part, so loop through all of it + for (i = 0; /* void */ ; i++) + { + if (i >= part->nelts) + { + if (part->next == NULL) + { + /* The last part, search is done. */ + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + //Just compare the lengths and then the names case insensitively. + if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) + { + /* This header doesn't match. */ + continue; + } + + /* + * Ta-da, we got one! + * Note, we've stopped the search at the first matched header + * while more then one header may match. + */ + return &h[i]; + } + + /* No headers was found */ + return NULL; +} + +/** + * Sample code from nginx + * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/#how-can-i-set-a-header + */ +ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) { + ngx_table_elt_t *h; + + /* + All we have to do is just to allocate the header... + */ + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + return NGX_ERROR; + } + + /* + ... setup the header key ... + */ + h->key = *key; + + /* + ... and the value. + */ + h->value = *value; + + /* + Mark the header as not deleted. + */ + h->hash = 1; + + return NGX_OK; +} \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_header_processing.h b/src/ngx_http_auth_jwt_header_processing.h new file mode 100644 index 0000000..0b64133 --- /dev/null +++ b/src/ngx_http_auth_jwt_header_processing.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#ifndef _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H +#define _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H + +ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); +ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); + +#endif /* _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H */ \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index a548996..9819791 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -1,6 +1,10 @@ /* - * Tesla Government - * @author joefitz + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/TeslaGov/ngx-http-auth-jwt-module */ #include @@ -10,22 +14,24 @@ #include +#include "ngx_http_auth_jwt_header_processing.h" +#include "ngx_http_auth_jwt_binary_converters.h" +#include "ngx_http_auth_jwt_string.h" + typedef struct { - ngx_str_t auth_jwt_loginurl; - ngx_str_t auth_jwt_key; - ngx_flag_t auth_jwt_enabled; - ngx_flag_t auth_jwt_redirect; + ngx_str_t auth_jwt_loginurl; + ngx_str_t auth_jwt_key; + ngx_flag_t auth_jwt_enabled; + ngx_flag_t auth_jwt_redirect; + ngx_str_t auth_jwt_validation_type; + } ngx_http_auth_jwt_loc_conf_t; static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf); static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r); static void * ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf); static char * ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); -static int hex_char_to_binary( char ch, char* ret ); -static int hex_to_binary( const char* str, u_char* buf, int len ); -static char * ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); -static ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr); -static ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type); static ngx_command_t ngx_http_auth_jwt_commands[] = { @@ -57,6 +63,13 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_redirect), NULL }, + { ngx_string("auth_jwt_validation_type"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validation_type), + NULL }, + ngx_null_command }; @@ -94,12 +107,8 @@ ngx_module_t ngx_http_auth_jwt_module = { static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { - ngx_str_t jwtCookieName = ngx_string("rampartjwt"); - ngx_str_t passportKeyCookieName = ngx_string("PassportKey"); ngx_str_t useridHeaderName = ngx_string("x-userid"); ngx_str_t emailHeaderName = ngx_string("x-email"); - ngx_int_t n; - ngx_str_t jwtCookieVal; char* jwtCookieValChrPtr; char* return_url; ngx_http_auth_jwt_loc_conf_t *jwtcf; @@ -121,30 +130,13 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) return NGX_DECLINED; } -// ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Key: %s, Enabled: %d", -// jwtcf->auth_jwt_key.data, -// jwtcf->auth_jwt_enabled); - - - // get the cookie - // TODO: the cookie name could be passed in dynamicallly - n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &jwtCookieName, &jwtCookieVal); - if (n == NGX_DECLINED) + jwtCookieValChrPtr = getJwt(r, jwtcf->auth_jwt_validation_type); + if (jwtCookieValChrPtr == NULL) { - // if we can't find the first cookie, check the legacy location - n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &passportKeyCookieName, &jwtCookieVal); - if (n == NGX_DECLINED) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); - goto redirect; - } + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); + goto redirect; } - // the cookie data is not necessarily null terminated... we need a null terminated character pointer - jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); - -// ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "rampartjwt: %s %d", jwtCookieValChrPtr, jwtCookieVal.len); - // convert key from hex to binary keyBinary = ngx_palloc(r->pool, jwtcf->auth_jwt_key.len / 2); if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) @@ -161,8 +153,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } -// ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "parsed jwt:\n%s", jwt_dump_str(jwt, 1)); - // validate the algorithm alg = jwt_get_alg(jwt); if (alg != JWT_ALG_HS256) @@ -281,14 +271,14 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; } - if (jwtcf->auth_jwt_redirect) - { - return NGX_HTTP_MOVED_TEMPORARILY; - } - else - { - return NGX_HTTP_UNAUTHORIZED; - } + if (jwtcf->auth_jwt_redirect) + { + return NGX_HTTP_MOVED_TEMPORARILY; + } + else + { + return NGX_HTTP_UNAUTHORIZED; + } } @@ -340,116 +330,65 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); + ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) { conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; } - if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) - { + if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) + { conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; - } - - ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Merged Location Configuration"); + } -// ngx_conf_log_error(NGX_LOG_ERR, cf, 0, "Key: %s, Enabled: %d", -// conf->auth_jwt_key.data, -// conf->auth_jwt_enabled); return NGX_CONF_OK; } -static int -hex_char_to_binary( char ch, char* ret ) +static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) { - ch = tolower( ch ); - if( isdigit( ch ) ) - *ret = ch - '0'; - else if( ch >= 'a' && ch <= 'f' ) - *ret = ( ch - 'a' ) + 10; - else if( ch >= 'A' && ch <= 'F' ) - *ret = ( ch - 'A' ) + 10; - else - return *ret = 0; - return 1; -} + static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); + ngx_table_elt_t *authorizationHeader; + char* jwtCookieValChrPtr = NULL; + ngx_str_t jwtCookieVal; + ngx_int_t n; + ngx_str_t authorizationHeaderStr; -static int -hex_to_binary( const char* str, u_char* buf, int len ) -{ - u_char - *cpy = buf; - char - low, - high; - int - odd = len % 2; - - if (odd) { - return -1; + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "auth_jwt_validation_type.len %d", auth_jwt_validation_type.len); + + if (auth_jwt_validation_type.len == 0 || (auth_jwt_validation_type.len == sizeof("AUTHORIZATION") - 1 && ngx_strncmp(auth_jwt_validation_type.data, "AUTHORIZATION", sizeof("AUTHORIZATION") - 1)==0)) + { + // using authorization header + authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); + if (authorizationHeader != NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); + + authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; + authorizationHeaderStr.len = authorizationHeader->value.len - (sizeof("Bearer ") - 1); + + jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); + } } + else if (auth_jwt_validation_type.len > sizeof("COOKIE=") && ngx_strncmp(auth_jwt_validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1)==0) + { + auth_jwt_validation_type.data += sizeof("COOKIE=") - 1; + auth_jwt_validation_type.len -= sizeof("COOKIE=") - 1; - for (int i = 0; i < len; i += 2) { - hex_char_to_binary( *(str + i), &high ); - hex_char_to_binary( *(str + i + 1 ), &low ); - - *cpy++ = low | (high << 4); + // get the cookie + // TODO: the cookie name could be passed in dynamicallly + n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &auth_jwt_validation_type, &jwtCookieVal); + if (n != NGX_DECLINED) + { + jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); + } } - return 0; -} -/** copies an nginx string structure to a newly allocated character pointer */ -static char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str) -{ - char* char_ptr = ngx_palloc(pool, str.len + 1); - ngx_memcpy(char_ptr, str.data, str.len); - *(char_ptr + str.len) = '\0'; - return char_ptr; + return jwtCookieValChrPtr; } -/** copies a character pointer string to an nginx string structure */ -static ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr) -{ - int len = strlen(char_ptr); - - ngx_str_t str_t; - str_t.data = ngx_palloc(pool, len); - ngx_memcpy(str_t.data, char_ptr, len); - str_t.len = len; - return str_t; -} -/** - * Sample code from nginx - * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/#how-can-i-set-a-header - */ -static ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) { - ngx_table_elt_t *h; - - /* - All we have to do is just to allocate the header... - */ - h = ngx_list_push(&r->headers_out.headers); - if (h == NULL) { - return NGX_ERROR; - } - - /* - ... setup the header key ... - */ - h->key = *key; - - /* - ... and the value. - */ - h->value = *value; - - /* - Mark the header as not deleted. - */ - h->hash = 1; - - return NGX_OK; -} diff --git a/src/ngx_http_auth_jwt_string.c b/src/ngx_http_auth_jwt_string.c new file mode 100644 index 0000000..186121f --- /dev/null +++ b/src/ngx_http_auth_jwt_string.c @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/TeslaGov/ngx-http-auth-jwt-module + */ +#include + +#include "ngx_http_auth_jwt_string.h" + +/** copies an nginx string structure to a newly allocated character pointer */ +char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str) +{ + char* char_ptr = ngx_palloc(pool, str.len + 1); + ngx_memcpy(char_ptr, str.data, str.len); + *(char_ptr + str.len) = '\0'; + return char_ptr; +} + +/** copies a character pointer string to an nginx string structure */ +ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr) +{ + int len = strlen(char_ptr); + + ngx_str_t str_t; + str_t.data = ngx_palloc(pool, len); + ngx_memcpy(str_t.data, char_ptr, len); + str_t.len = len; + return str_t; +} \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_string.h b/src/ngx_http_auth_jwt_string.h new file mode 100644 index 0000000..594785b --- /dev/null +++ b/src/ngx_http_auth_jwt_string.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Tesla Government + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/TeslaGov/ngx-http-auth-jwt-module + */ + +#ifndef _NGX_HTTP_AUTH_JWT_STRING_H +#define _NGX_HTTP_AUTH_JWT_STRING_H + +#include + +char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); +ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr); + +#endif /* _NGX_HTTP_AUTH_JWT_STRING_H */ \ No newline at end of file From 6e1f280420a79e96f052e8589def9a26e8dbc676 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 6 Feb 2018 18:05:28 -0500 Subject: [PATCH 028/130] Typo for test status Typo for test status --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 019e89a..0362e60 100755 --- a/build.sh +++ b/build.sh @@ -50,7 +50,7 @@ else fi TEST_SECURE_HEADER_EXPECT_302=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-auth-header/index.html -H 'cache-control: no-cache'` -if [ "$TEST_SECURE_HEADER_EXPECT_302" -eq "200" ];then +if [ "$TEST_SECURE_HEADER_EXPECT_302" -eq "302" ];then echo -e "${GREEN}Secure test without jwt auth header pass ${TEST_SECURE_HEADER_EXPECT_302}${NONE}"; else echo -e "${RED}Secure test without jwt auth header fail ${TEST_SECURE_HEADER_EXPECT_302}${NONE}"; From 6aa585fac5197ea8b84bf1c1bf84bf6b0263419a Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 6 Feb 2018 18:12:18 -0500 Subject: [PATCH 029/130] Update README.md --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 217f7cd..7e07429 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,18 @@ auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; ``` -So, a typical use would be to specify the key and loginurl on the main level and then only turn on the locations that you want to secure (not the login page). +So, a typical use would be to specify the key and loginurl on the main level and then only turn on the locations that you want to secure (not the login page). Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. + +``` +auth_jwt_redirect off; +``` +If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. + +``` +auth_jwt_validation_type AUTHORIZATION; +auth_jwt_validation_type COOKIE=rampartjwt; +``` +By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. The Dockerfile builds all of the dependencies as well as the module, downloads a binary version of nginx, and runs the module as a dynamic module. From 8fcda49aea4f4cf9ad3ac6589fc82fdc012d357e Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 9 Apr 2018 20:52:35 -0400 Subject: [PATCH 030/130] only set headers for sub and email if they exist in the jet (#29) --- build.sh | 18 ++++++++++++++++++ src/ngx_http_auth_jwt_module.c | 14 ++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/build.sh b/build.sh index 0362e60..4abe819 100755 --- a/build.sh +++ b/build.sh @@ -20,6 +20,8 @@ MACHINE_IP=`docker-machine ip` docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 +MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q +MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM TEST_INSECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000 -H 'cache-control: no-cache'` if [ "$TEST_INSECURE_EXPECT_200" -eq "200" ];then @@ -62,3 +64,19 @@ if [ "$TEST_SECURE_NO_REDIRECT_EXPECT_401" -eq "401" ];then else echo -e "${RED}Secure test without jwt no redirect fail ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; fi + +TEST_WITH_NO_SUB_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${MISSING_SUB_JWT}"` +if [ "$TEST_WITH_NO_SUB_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with jwt cookie pass ${TEST_WITH_NO_SUB_EXPECT_200}${NONE}"; +else + echo -e "${RED}Secure test with jwt cookie fail ${TEST_WITH_NO_SUB_EXPECT_200}${NONE}"; +fi + +TEST_WITH_NO_EMAIL_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${MISSING_EMAIL_JWT}"` +if [ "$TEST_WITH_NO_EMAIL_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with jwt cookie pass ${TEST_WITH_NO_EMAIL_EXPECT_200}${NONE}"; +else + echo -e "${RED}Secure test with jwt cookie fail ${TEST_WITH_NO_EMAIL_EXPECT_200}${NONE}"; +fi + + diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 9819791..d217cf5 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -176,16 +176,22 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain a subject"); } - sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); - set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); + else + { + sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); + set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); + } email = jwt_get_grant(jwt, "emailAddress"); if (email == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); } - email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); - set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); + else + { + email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); + set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); + } return NGX_OK; From 2ab3fd32dddd93b6f4afb025273e1952ca0e5605 Mon Sep 17 00:00:00 2001 From: Tim Underhay <15734900+citizentim@users.noreply.github.com> Date: Wed, 25 Apr 2018 16:16:27 -0600 Subject: [PATCH 031/130] RSA Key Validation Support (#30) * First stab at RSA validation. * Fix for build errors * Another build fix * Fix for key copy * Key length fix * Logging * Logging fix * Remove debug logs. Make validation of email optional with auth_jwt_validate_email * Fix for email validation option, now auth_jwt_email_validation * Changed back to auth_jwt_validate_email and additional conf merging code * One more email validation fix * More fixes to email validate * Another fix * Set getJwt logs to NGX_LOG_DEBUG * Updated README. Rearranged some code. * Added else error condition to avert compiler warning. --- README.md | 30 ++++++++++++++ src/ngx_http_auth_jwt_module.c | 76 ++++++++++++++++++++++++++-------- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7e07429..7de9088 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ This module requires several new nginx.conf directives, which can be specified i auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; +auth_jwt_algorithm HS256; # or RS256 +auth_jwt_validate_email on; # or off ``` So, a typical use would be to specify the key and loginurl on the main level and then only turn on the locations that you want to secure (not the login page). Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. @@ -28,6 +30,34 @@ auth_jwt_validation_type COOKIE=rampartjwt; ``` By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. + + +The default algorithm is 'HS256', for symmetric key validation. Also supported is 'RS256', for RSA 256-bit public key validation. + +If using "auth_jwt_algorithm RS256;", then the 'auth_jwt_key' field must be set to your public key. That is the public key, rather than a PEM certificate. I.e.: + +``` +auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0aPPpS7ufs0bGbW9+OFQ +RvJwb58fhi2BuHMd7Ys6m8D1jHW/AhDYrYVZtUnA60lxwSJ/ZKreYOQMlNyZfdqA +rhYyyUkedDn8e0WsDvH+ocY0cMcxCCN5jItCwhIbIkTO6WEGrDgWTY57UfWDqbMZ +4lMn42f77OKFoxsOA6CVvpsvrprBPIRPa25H2bJHODHEtDr/H519Y681/eCyeQE/ +1ibKL2cMN49O7nRAAaUNoFcO89Uc+GKofcad1TTwtTIwmSMbCLVkzGeExBCrBTQo +wO6AxLijfWV/JnVxNMUiobiKGc/PP6T5PI70Uv67Y4FzzWTuhqmREb3/BlcbPwtM +oQIDAQAB +-----END PUBLIC KEY-----"; +``` + + + +By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different user identifier property such as 'sub', set: + +``` +auth_jwt_validate_email off; +``` + + + The Dockerfile builds all of the dependencies as well as the module, downloads a binary version of nginx, and runs the module as a dynamic module. Have a look at build.sh, which creates the docker image and container and executes some test requests to illustrate that some pages are secured by the module and requre a valid JWT. diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index d217cf5..b64c12b 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -24,6 +24,8 @@ typedef struct { ngx_flag_t auth_jwt_enabled; ngx_flag_t auth_jwt_redirect; ngx_str_t auth_jwt_validation_type; + ngx_str_t auth_jwt_algorithm; + ngx_flag_t auth_jwt_validate_email; } ngx_http_auth_jwt_loc_conf_t; @@ -70,6 +72,20 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validation_type), NULL }, + { ngx_string("auth_jwt_algorithm"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_algorithm), + NULL }, + + { ngx_string("auth_jwt_validate_email"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validate_email), + NULL }, + ngx_null_command }; @@ -122,6 +138,8 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_str_t email_t; time_t exp; time_t now; + ngx_str_t auth_jwt_algorithm; + int keylen; jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); @@ -137,16 +155,34 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } - // convert key from hex to binary - keyBinary = ngx_palloc(r->pool, jwtcf->auth_jwt_key.len / 2); - if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) + // convert key from hex to binary, if a symmetric key + + auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; + if (auth_jwt_algorithm.len == 0 || (auth_jwt_algorithm.len == sizeof("HS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "HS256", sizeof("HS256") - 1)==0)) + { + keylen = jwtcf->auth_jwt_key.len / 2; + keyBinary = ngx_palloc(r->pool, keylen); + if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); + goto redirect; + } + } + else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); + // in this case, 'Binary' is a misnomer, as it is the public key string itself + keyBinary = ngx_palloc(r->pool, jwtcf->auth_jwt_key.len); + ngx_memcpy(keyBinary, jwtcf->auth_jwt_key.data, jwtcf->auth_jwt_key.len); + keylen = jwtcf->auth_jwt_key.len; + } + else + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm"); goto redirect; } // validate the jwt - jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, jwtcf->auth_jwt_key.len / 2); + jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, keylen); if (jwtParseReturnCode != 0) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt"); @@ -155,7 +191,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // validate the algorithm alg = jwt_get_alg(jwt); - if (alg != JWT_ALG_HS256) + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_RS256) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in jwt %d", alg); goto redirect; @@ -182,15 +218,18 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); } - email = jwt_get_grant(jwt, "emailAddress"); - if (email == NULL) + if (jwtcf->auth_jwt_validate_email == 1) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); - } - else - { - email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); - set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); + email = jwt_get_grant(jwt, "emailAddress"); + if (email == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); + } + else + { + email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); + set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); + } } return NGX_OK; @@ -321,6 +360,7 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) // set the flag to unset conf->auth_jwt_enabled = (ngx_flag_t) -1; conf->auth_jwt_redirect = (ngx_flag_t) -1; + conf->auth_jwt_validate_email = (ngx_flag_t) -1; ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); @@ -337,6 +377,8 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); + ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); + ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) { @@ -360,7 +402,7 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) ngx_int_t n; ngx_str_t authorizationHeaderStr; - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "auth_jwt_validation_type.len %d", auth_jwt_validation_type.len); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "auth_jwt_validation_type.len %d", auth_jwt_validation_type.len); if (auth_jwt_validation_type.len == 0 || (auth_jwt_validation_type.len == sizeof("AUTHORIZATION") - 1 && ngx_strncmp(auth_jwt_validation_type.data, "AUTHORIZATION", sizeof("AUTHORIZATION") - 1)==0)) { @@ -368,14 +410,14 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); if (authorizationHeader != NULL) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; authorizationHeaderStr.len = authorizationHeader->value.len - (sizeof("Bearer ") - 1); jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); } } else if (auth_jwt_validation_type.len > sizeof("COOKIE=") && ngx_strncmp(auth_jwt_validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1)==0) From 4aab233940df5faefb61ee22f96b12a86e132339 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Wed, 25 Apr 2018 18:17:22 -0400 Subject: [PATCH 032/130] Test for RSA Support * test for rs256 --- Dockerfile | 1 + build.sh | 8 ++++++++ resources/rsa_key_2048-pub.pem | 9 +++++++++ resources/rsa_key_2048.pem | 28 ++++++++++++++++++++++++++++ resources/test-jwt-nginx.conf | 17 +++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100755 resources/rsa_key_2048-pub.pem create mode 100755 resources/rsa_key_2048.pem diff --git a/Dockerfile b/Dockerfile index 4d61a23..963f559 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,6 +99,7 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ COPY resources/nginx.conf /etc/nginx/nginx.conf COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure +RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect diff --git a/build.sh b/build.sh index 4abe819..0225715 100755 --- a/build.sh +++ b/build.sh @@ -22,6 +22,7 @@ docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM +VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ TEST_INSECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000 -H 'cache-control: no-cache'` if [ "$TEST_INSECURE_EXPECT_200" -eq "200" ];then @@ -79,4 +80,11 @@ else echo -e "${RED}Secure test with jwt cookie fail ${TEST_WITH_NO_EMAIL_EXPECT_200}${NONE}"; fi +TEST_SECURE_RS256_COOKIE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-rs256/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALID_RS256_JWT}"` +if [ "$TEST_SECURE_RS256_COOKIE_EXPECT_200" -eq "200" ];then + echo -e "${GREEN}Secure test with rs256 jwt cookie pass ${TEST_SECURE_RS256_COOKIE_EXPECT_200}${NONE}"; +else + echo -e "${RED}Secure test with rs256 jwt cookie fail ${TEST_SECURE_RS256_COOKIE_EXPECT_200}${NONE}"; +fi + diff --git a/resources/rsa_key_2048-pub.pem b/resources/rsa_key_2048-pub.pem new file mode 100755 index 0000000..01f59bf --- /dev/null +++ b/resources/rsa_key_2048-pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh +uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ +iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM +ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g +6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe +K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t +BwIDAQAB +-----END PUBLIC KEY----- diff --git a/resources/rsa_key_2048.pem b/resources/rsa_key_2048.pem new file mode 100755 index 0000000..0f58120 --- /dev/null +++ b/resources/rsa_key_2048.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDC2kwAziXUf33m +iqWp0yG6o259+nj7hpQLC4UT0Hmz0wmvreDJ/yNbSgOvsxvVdvzL2IaRZ+Gi5mo0 +lswWvL6IGz7PZO0kXTq9sdBnNqMOx27HddV9e/2/p0MgibJTbgywY2Sk23QYhJpq +Kq/nU0xlBfSaI5ddZ2RC9ZNkVeGawUKYksTruhAVJqviHN8BoK6VowP5vcxyyOWH +TK9KruDqzCIhqwRTeo0spokBkTN/LCuhVivcHAzUiJVtB4qAiTI9L/zkzhjpKz9P +45aLU54rj011gG8U/6E1USh5nMnPkr+d3oLfkhfS3Zs3kJVdyFQWZpQxiTaI92Fd +2wLvbS0HAgMBAAECggEAD8dTnkETSSjlzhRuI9loAtAXM3Zj86JLPLW7GgaoxEoT +n7lJ2bGicFMHB2ROnbOb9vnas82gtOtJsGaBslmoaCckp/C5T1eJWTEb+i+vdpPp +wZcmKZovyyRFSE4+NYlU17fEv6DRvuaGBpDcW7QgHJIl45F8QWEM+msee2KE+V4G +z/9vAQ+sOlvsb4mJP1tJIBx9Lb5loVREwCRy2Ha9tnWdDNar8EYkOn8si4snPT+E +3ZCy8mlcZyUkZeiS/HdtydxZfoiwrSRYamd1diQpPhWCeRteQ802a7ds0Y2YzgfF +UaYjNuRQm7zA//hwbXS7ELPyNMU15N00bajlG0tUOQKBgQDnLy01l20OneW6A2cI +DIDyYhy5O7uulsaEtJReUlcjEDMkin8b767q2VZHb//3ZH+ipnRYByUUyYUhdOs2 +DYRGGeAebnH8wpTT4FCYxUsIUpDfB7RwfdBONgaKewTJz/FPswy1Ye0b5H2c6vVi +m2FZ33HQcoZ3wvFFqyGVnMzpOwKBgQDXxL95yoxUGKa8vMzcE3Cn01szh0dFq0sq +cFpM+HWLVr84CItuG9H6L0KaStEEIOiJsxOVpcXfFFhsJvOGhMA4DQTwH4WuXmXp +1PoVMDlV65PYqvhzwL4+QhvZO2bsrEunITXOmU7CI6kilnAN3LuP4HbqZgoX9lqP +I31VYzLupQKBgGEYck9w0s/xxxtR9ILv5XRnepLdoJzaHHR991aKFKjYU/KD7JDK +INfoAhGs23+HCQhCCtkx3wQVA0Ii/erM0II0ueluD5fODX3TV2ZibnoHW2sgrEsW +vFcs36BnvIIaQMptc+f2QgSV+Z/fGsKYadG6Q+39O7au/HB7SHayzWkjAoGBAMgt +Fzslp9TpXd9iBWjzfCOnGUiP65Z+GWkQ/SXFqD+SRir0+m43zzGdoNvGJ23+Hd6K +TdQbDJ0uoe4MoQeepzoZEgi4JeykVUZ/uVfo+nh06yArVf8FxTm7WVzLGGzgV/uA ++wtl/cRtEyAsk1649yW/KHPEIP8kJdYAJeoO8xSlAoGAERMrkFR7KGYZG1eFNRdV +mJMq+Ibxyw8ks/CbiI+n3yUyk1U8962ol2Q0T4qjBmb26L5rrhNQhneM4e8mo9FX +LlQapYkPvkdrqW0Bp72A/UNAvcGTmN7z5OCJGMUutx2hmEAlrYmpLKS8pM/p9zpK +tEOtzsP5GMDYVlEp1jYSjzQ= +-----END PRIVATE KEY----- diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index c0acdf2..b39eb95 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -27,6 +27,23 @@ server { index index.html index.htm; } + location ~ ^/secure-rs256/ { + auth_jwt_enabled on; + auth_jwt_validation_type COOKIE=rampartjwt; + auth_jwt_algorithm RS256; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh +uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ +iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM +ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g +6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe +K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t +BwIDAQAB +-----END PUBLIC KEY-----"; + root /usr/share/nginx; + index index.html index.htm; + } + location / { root /usr/share/nginx/html; index index.html index.htm; From 7593b72696aabf81aadb9f180e791313273458e4 Mon Sep 17 00:00:00 2001 From: Tim Underhay <15734900+citizentim@users.noreply.github.com> Date: Thu, 24 May 2018 12:46:24 -0600 Subject: [PATCH 033/130] build.sh fix for MacOS Docker. Removed memcpy op for RSA validation and use pointer instead --- build.sh | 4 +++- ngx_http_auth_jwt_module.so | Bin 177360 -> 178096 bytes src/ngx_http_auth_jwt_module.c | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index 0225715..7e6bdc4 100755 --- a/build.sh +++ b/build.sh @@ -15,7 +15,9 @@ fi CONTAINER_ID=$(docker run --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${DOCKER_IMAGE_NAME}) -MACHINE_IP=`docker-machine ip` +if ! MACHINE_IP=`docker-machine ip 2>/dev/null`; then + MACHINE_IP='0.0.0.0' # fix for MacOS +fi docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . diff --git a/ngx_http_auth_jwt_module.so b/ngx_http_auth_jwt_module.so index 6125fcaeabe4ccf751860dbcaed1a637ff216ebe..8d9c0840a8d03e1e9ec6607771e610f99fd6653e 100755 GIT binary patch delta 89688 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

U$TF3crk`x2+*IKRZlV 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 Zbktq4VK>i{UWlh$v{i?VklhgW{{YPZe&GNB delta 88739 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 KWpYFVZ}pXa diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index b64c12b..e5677e4 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -171,8 +171,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) { // in this case, 'Binary' is a misnomer, as it is the public key string itself - keyBinary = ngx_palloc(r->pool, jwtcf->auth_jwt_key.len); - ngx_memcpy(keyBinary, jwtcf->auth_jwt_key.data, jwtcf->auth_jwt_key.len); + keyBinary = jwtcf->auth_jwt_key.data; keylen = jwtcf->auth_jwt_key.len; } else From e5df258a19ed859b89ada77638427b3cd9a4cae1 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Fri, 25 May 2018 11:59:23 -0400 Subject: [PATCH 034/130] I had checked this in by mistake at some point. --- ngx_http_auth_jwt_module.so | Bin 178096 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 ngx_http_auth_jwt_module.so diff --git a/ngx_http_auth_jwt_module.so b/ngx_http_auth_jwt_module.so deleted file mode 100755 index 8d9c0840a8d03e1e9ec6607771e610f99fd6653e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178096 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 Date: Wed, 30 May 2018 19:40:27 -0400 Subject: [PATCH 035/130] call jwt_free --- src/ngx_http_auth_jwt_module.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index e5677e4..db803af 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -129,7 +129,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) char* return_url; ngx_http_auth_jwt_loc_conf_t *jwtcf; u_char *keyBinary; - jwt_t *jwt; + jwt_t *jwt = NULL; int jwtParseReturnCode; jwt_alg_t alg; const char* sub; @@ -231,9 +231,17 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } } + jwt_free(jwt); + return NGX_OK; redirect: + + if (jwt) + { + jwt_free(jwt); + } + r->headers_out.location = ngx_list_push(&r->headers_out.headers); if (r->headers_out.location == NULL) From 0686f3a7aba0871040f0d2fcca62ec8c75feda68 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Fri, 8 Jun 2018 15:01:48 -0400 Subject: [PATCH 036/130] bump libjwt version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 963f559..587c6a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ make install # build libjwt -ARG LIBJWT_VERSION=1.8.0 +ARG LIBJWT_VERSION=1.9.0 RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ unzip v$LIBJWT_VERSION.zip && \ rm v$LIBJWT_VERSION.zip && \ From 5f244f86366915b4b4c42de853415a24b7e96fb7 Mon Sep 17 00:00:00 2001 From: Maxime Date: Fri, 8 Jun 2018 22:04:30 +0200 Subject: [PATCH 037/130] 37: Removed auth_jwt_redirect and auth_jwt_loginurl directives --- .gitignore | 1 + README.md | 13 ++- resources/test-jwt-nginx.conf | 31 +++---- src/ngx_http_auth_jwt_module.c | 163 +++++---------------------------- 4 files changed, 47 insertions(+), 161 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a51369c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ngx_http_auth_jwt_module.so diff --git a/README.md b/README.md index 7de9088..b74fb07 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,23 @@ This module requires several new nginx.conf directives, which can be specified i ``` auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; -auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; auth_jwt_algorithm HS256; # or RS256 auth_jwt_validate_email on; # or off ``` -So, a typical use would be to specify the key and loginurl on the main level and then only turn on the locations that you want to secure (not the login page). Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. +So, a typical use would be to specify the key on the main level and then only turn on the locations that you want to secure (not the login page). Unauthorized requests are given 401 "Unauthorized" responses, you can redirect them with the nginx's `error_page` directive. ``` -auth_jwt_redirect off; +location @login_redirect { + allow all; + return 302 https://yourdomain.com/loginpage; +} + +location /secure-location/ { + auth_jwt_enabled on; + error_page 401 = @login_redirect; +} ``` If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index b39eb95..b0f1f9d 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -1,33 +1,38 @@ server { auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; - auth_jwt_loginurl "https://teslagov.com"; + set $auth_jwt_login_url "https://teslagov.com"; auth_jwt_enabled off; - auth_jwt_redirect on; listen 8000; server_name localhost; + root /usr/share/nginx/html; + index index.html index.htm; + + location @login_redirect { + return 302 $auth_jwt_login_url?redirect=$request_uri&$args; + } + location ~ ^/secure-no-redirect/ { + rewrite "" / break; auth_jwt_enabled on; - auth_jwt_redirect off; - root /usr/share/nginx; - index index.html index.htm; } location ~ ^/secure/ { + rewrite "" / break; auth_jwt_enabled on; auth_jwt_validation_type COOKIE=rampartjwt; - root /usr/share/nginx; - index index.html index.htm; + error_page 401 = @login_redirect; } location ~ ^/secure-auth-header/ { + rewrite "" / break; auth_jwt_enabled on; - root /usr/share/nginx; - index index.html index.htm; + error_page 401 = @login_redirect; } location ~ ^/secure-rs256/ { + rewrite "" / break; auth_jwt_enabled on; auth_jwt_validation_type COOKIE=rampartjwt; auth_jwt_algorithm RS256; @@ -40,13 +45,7 @@ ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t BwIDAQAB -----END PUBLIC KEY-----"; - root /usr/share/nginx; - index index.html index.htm; } - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } + location / {} } - diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index db803af..213d410 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -19,10 +19,8 @@ #include "ngx_http_auth_jwt_string.h" typedef struct { - ngx_str_t auth_jwt_loginurl; ngx_str_t auth_jwt_key; ngx_flag_t auth_jwt_enabled; - ngx_flag_t auth_jwt_redirect; ngx_str_t auth_jwt_validation_type; ngx_str_t auth_jwt_algorithm; ngx_flag_t auth_jwt_validate_email; @@ -37,13 +35,6 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type); static ngx_command_t ngx_http_auth_jwt_commands[] = { - { ngx_string("auth_jwt_loginurl"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_loginurl), - NULL }, - { ngx_string("auth_jwt_key"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -58,13 +49,6 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_enabled), NULL }, - { ngx_string("auth_jwt_redirect"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_redirect), - NULL }, - { ngx_string("auth_jwt_validation_type"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -126,7 +110,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_str_t useridHeaderName = ngx_string("x-userid"); ngx_str_t emailHeaderName = ngx_string("x-email"); char* jwtCookieValChrPtr; - char* return_url; ngx_http_auth_jwt_loc_conf_t *jwtcf; u_char *keyBinary; jwt_t *jwt = NULL; @@ -140,21 +123,21 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) time_t now; ngx_str_t auth_jwt_algorithm; int keylen; - + jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); - - if (!jwtcf->auth_jwt_enabled) + + if (!jwtcf->auth_jwt_enabled) { return NGX_DECLINED; } - + jwtCookieValChrPtr = getJwt(r, jwtcf->auth_jwt_validation_type); if (jwtCookieValChrPtr == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); - goto redirect; + return NGX_HTTP_UNAUTHORIZED; } - + // convert key from hex to binary, if a symmetric key auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; @@ -165,7 +148,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); - goto redirect; + return NGX_HTTP_UNAUTHORIZED; } } else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) @@ -177,32 +160,32 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) else { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm"); - goto redirect; + return NGX_HTTP_UNAUTHORIZED; } - + // validate the jwt jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, keylen); if (jwtParseReturnCode != 0) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt"); - goto redirect; + return NGX_HTTP_UNAUTHORIZED; } - + // validate the algorithm alg = jwt_get_alg(jwt); if (alg != JWT_ALG_HS256 && alg != JWT_ALG_RS256) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in jwt %d", alg); - goto redirect; + return NGX_HTTP_UNAUTHORIZED; } - + // validate the exp date of the JWT exp = (time_t)jwt_get_grant_int(jwt, "exp"); now = time(NULL); if (exp < now) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt has expired"); - goto redirect; + return NGX_HTTP_UNAUTHORIZED; } // extract the userid @@ -234,103 +217,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_free(jwt); return NGX_OK; - - redirect: - - if (jwt) - { - jwt_free(jwt); - } - - r->headers_out.location = ngx_list_push(&r->headers_out.headers); - - if (r->headers_out.location == NULL) - { - ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); - } - - r->headers_out.location->hash = 1; - r->headers_out.location->key.len = sizeof("Location") - 1; - r->headers_out.location->key.data = (u_char *) "Location"; - - if (r->method == NGX_HTTP_GET) - { - int loginlen; - char * scheme; - ngx_str_t server; - ngx_str_t uri_variable_name = ngx_string("request_uri"); - ngx_int_t uri_variable_hash; - ngx_http_variable_value_t * request_uri_var; - ngx_str_t uri; - ngx_str_t uri_escaped; - uintptr_t escaped_len; - - loginlen = jwtcf->auth_jwt_loginurl.len; - - scheme = (r->connection->ssl) ? "https" : "http"; - server = r->headers_in.server; - - // get the URI - uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); - request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); - - // get the URI - if(request_uri_var && !request_uri_var->not_found && request_uri_var->valid) - { - // ideally we would like the uri with the querystring parameters - uri.data = ngx_palloc(r->pool, request_uri_var->len); - uri.len = request_uri_var->len; - ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "found uri with querystring %s", ngx_str_t_to_char_ptr(r->pool, uri)); - } - else - { - // fallback to the querystring without params - uri = r->uri; - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "fallback to querystring without params"); - } - - // escape the URI - escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; - uri_escaped.data = ngx_palloc(r->pool, escaped_len); - uri_escaped.len = escaped_len; - ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); - - r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; - return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); - ngx_memcpy(return_url, jwtcf->auth_jwt_loginurl.data, jwtcf->auth_jwt_loginurl.len); - int return_url_idx = jwtcf->auth_jwt_loginurl.len; - ngx_memcpy(return_url+return_url_idx, "?return_url=", sizeof("?return_url=") - 1); - return_url_idx += sizeof("?return_url=") - 1; - ngx_memcpy(return_url+return_url_idx, scheme, strlen(scheme)); - return_url_idx += strlen(scheme); - ngx_memcpy(return_url+return_url_idx, "://", sizeof("://") - 1); - return_url_idx += sizeof("://") - 1; - ngx_memcpy(return_url+return_url_idx, server.data, server.len); - return_url_idx += server.len; - ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); - return_url_idx += uri_escaped.len; - r->headers_out.location->value.data = (u_char *)return_url; - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "return_url: %s", ngx_str_t_to_char_ptr(r->pool, r->headers_out.location->value)); - } - else - { - // for non-get requests, redirect to the login page without a return URL - r->headers_out.location->value.len = jwtcf->auth_jwt_loginurl.len; - r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; - } - - if (jwtcf->auth_jwt_redirect) - { - return NGX_HTTP_MOVED_TEMPORARILY; - } - else - { - return NGX_HTTP_UNAUTHORIZED; - } } @@ -342,7 +228,7 @@ static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf) cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); - if (h == NULL) + if (h == NULL) { return NGX_ERROR; } @@ -359,18 +245,17 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) ngx_http_auth_jwt_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_jwt_loc_conf_t)); - if (conf == NULL) + if (conf == NULL) { return NULL; } - + // set the flag to unset conf->auth_jwt_enabled = (ngx_flag_t) -1; - conf->auth_jwt_redirect = (ngx_flag_t) -1; conf->auth_jwt_validate_email = (ngx_flag_t) -1; ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); - + return conf; } @@ -381,20 +266,14 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_http_auth_jwt_loc_conf_t *prev = parent; ngx_http_auth_jwt_loc_conf_t *conf = child; - ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); - - if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) - { - conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; - } - if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) + if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) { - conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; + conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; } return NGX_CONF_OK; @@ -435,7 +314,7 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) // get the cookie // TODO: the cookie name could be passed in dynamicallly n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &auth_jwt_validation_type, &jwtCookieVal); - if (n != NGX_DECLINED) + if (n != NGX_DECLINED) { jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); } From df7008a754cc4a64355b560a6ce6080bab787027 Mon Sep 17 00:00:00 2001 From: Maxime Date: Fri, 8 Jun 2018 22:08:50 +0200 Subject: [PATCH 038/130] Removed a deleted direcitve reference in readme. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b74fb07..83d0a33 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ location /secure-location/ { error_page 401 = @login_redirect; } ``` -If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. ``` auth_jwt_validation_type AUTHORIZATION; From cfea5700b629dc589cc4c0db01c57f9b4f6b9002 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Fri, 8 Jun 2018 23:51:55 -0400 Subject: [PATCH 039/130] allow options requests I've started using a different subdomain for my front end and back end. This has made my app perofrm CORS OPTIONS requests. When the request is made, it does not pass the token! I considered configuring NGINX to handle the OPTIONS requests, but I'd rather let my app handle it. --- src/ngx_http_auth_jwt_module.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index db803af..a44e3b9 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -147,6 +147,12 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { return NGX_DECLINED; } + + // pass through options requests without token authentication + if (r->method == NGX_HTTP_OPTIONS) + { + return NGX_DECLINED; + } jwtCookieValChrPtr = getJwt(r, jwtcf->auth_jwt_validation_type); if (jwtCookieValChrPtr == NULL) From 752ca9c81c200f45da0602358a40f3fbbc2048e6 Mon Sep 17 00:00:00 2001 From: Maxime Date: Mon, 4 Jun 2018 23:05:36 +0200 Subject: [PATCH 040/130] Use a bash function for tests --- .gitignore | 1 + build.sh | 83 +++--------------------------------------------------- test.sh | 61 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 79 deletions(-) create mode 100644 .gitignore create mode 100755 test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a51369c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ngx_http_auth_jwt_module.so diff --git a/build.sh b/build.sh index 7e6bdc4..44c5734 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash + +set -e RED='\033[01;31m' -GREEN='\033[01;32m' NONE='\033[00m' # build @@ -13,80 +14,4 @@ then exit 1; fi -CONTAINER_ID=$(docker run --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${DOCKER_IMAGE_NAME}) - -if ! MACHINE_IP=`docker-machine ip 2>/dev/null`; then - MACHINE_IP='0.0.0.0' # fix for MacOS -fi - -docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . - -VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 -MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q -MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM -VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ - -TEST_INSECURE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000 -H 'cache-control: no-cache'` -if [ "$TEST_INSECURE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Insecure test pass ${TEST_INSECURE_EXPECT_200}${NONE}"; -else - echo -e "${RED}Insecure test fail ${TEST_INSECURE_EXPECT_200}${NONE}"; -fi - -TEST_SECURE_COOKIE_EXPECT_302=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache'` -if [ "$TEST_SECURE_COOKIE_EXPECT_302" -eq "302" ];then - echo -e "${GREEN}Secure test without jwt cookie pass ${TEST_SECURE_COOKIE_EXPECT_302}${NONE}"; -else - echo -e "${RED}Secure test without jwt cookie fail ${TEST_SECURE_COOKIE_EXPECT_302}${NONE}"; -fi - -TEST_SECURE_COOKIE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALIDJWT}"` -if [ "$TEST_SECURE_COOKIE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt cookie pass ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; -else - echo -e "${RED}Secure test with jwt cookie fail ${TEST_SECURE_COOKIE_EXPECT_200}${NONE}"; -fi - -TEST_SECURE_HEADER_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-auth-header/index.html -H 'cache-control: no-cache' --header "Authorization: Bearer ${VALIDJWT}"` -if [ "$TEST_SECURE_HEADER_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt auth header pass ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; -else - echo -e "${RED}Secure test with jwt auth header fail ${TEST_SECURE_HEADER_EXPECT_200}${NONE}"; -fi - -TEST_SECURE_HEADER_EXPECT_302=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-auth-header/index.html -H 'cache-control: no-cache'` -if [ "$TEST_SECURE_HEADER_EXPECT_302" -eq "302" ];then - echo -e "${GREEN}Secure test without jwt auth header pass ${TEST_SECURE_HEADER_EXPECT_302}${NONE}"; -else - echo -e "${RED}Secure test without jwt auth header fail ${TEST_SECURE_HEADER_EXPECT_302}${NONE}"; -fi - -TEST_SECURE_NO_REDIRECT_EXPECT_401=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-no-redirect/index.html -H 'cache-control: no-cache'` -if [ "$TEST_SECURE_NO_REDIRECT_EXPECT_401" -eq "401" ];then - echo -e "${GREEN}Secure test without jwt no redirect pass ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; -else - echo -e "${RED}Secure test without jwt no redirect fail ${TEST_SECURE_NO_REDIRECT_EXPECT_401}${NONE}"; -fi - -TEST_WITH_NO_SUB_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${MISSING_SUB_JWT}"` -if [ "$TEST_WITH_NO_SUB_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt cookie pass ${TEST_WITH_NO_SUB_EXPECT_200}${NONE}"; -else - echo -e "${RED}Secure test with jwt cookie fail ${TEST_WITH_NO_SUB_EXPECT_200}${NONE}"; -fi - -TEST_WITH_NO_EMAIL_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${MISSING_EMAIL_JWT}"` -if [ "$TEST_WITH_NO_EMAIL_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with jwt cookie pass ${TEST_WITH_NO_EMAIL_EXPECT_200}${NONE}"; -else - echo -e "${RED}Secure test with jwt cookie fail ${TEST_WITH_NO_EMAIL_EXPECT_200}${NONE}"; -fi - -TEST_SECURE_RS256_COOKIE_EXPECT_200=`curl -X GET -o /dev/null --silent --head --write-out '%{http_code}\n' http://${MACHINE_IP}:8000/secure-rs256/index.html -H 'cache-control: no-cache' --cookie "rampartjwt=${VALID_RS256_JWT}"` -if [ "$TEST_SECURE_RS256_COOKIE_EXPECT_200" -eq "200" ];then - echo -e "${GREEN}Secure test with rs256 jwt cookie pass ${TEST_SECURE_RS256_COOKIE_EXPECT_200}${NONE}"; -else - echo -e "${RED}Secure test with rs256 jwt cookie fail ${TEST_SECURE_RS256_COOKIE_EXPECT_200}${NONE}"; -fi - - +./test.sh diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..d4c8b62 --- /dev/null +++ b/test.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +RED='\033[01;31m' +GREEN='\033[01;32m' +NONE='\033[00m' + +DOCKER_IMAGE_NAME=jwt-nginx +CONTAINER_ID=$(docker run --rm --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${DOCKER_IMAGE_NAME}) + +if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "linux"* ]]; then + # Mac OSX / Linux + MACHINE_IP='localhost' +else + # Windows + MACHINE_IP=`docker-machine ip 2> /dev/null` +fi + +docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . + +VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 +MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q +MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM +VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ + +test_jwt () { + name=$1 + path=$2 + expect=$3 + extra=$4 + + cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://$MACHINE_IP:8000$path -H 'cache-control: no-cache' $extra" + + test=$( eval ${cmd} ) + if [ "$test" -eq "$expect" ];then + echo -e "${GREEN}${name}: passed (${test})${NONE}"; + else + echo -e "${RED}${name}: failed (${test})${NONE}"; + fi +} + +test_jwt "Insecure test" "/" "200" + +test_jwt "Secure test without jwt cookie" "/secure/" "302" + +test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"rampartjwt=${VALIDJWT}\"" + +test_jwt "Secure test with jwt auth header" "/secure-auth-header/" "200" "--header \"Authorization: Bearer ${VALIDJWT}\"" + +test_jwt "Secure test without jwt auth header" "/secure-auth-header/" "302" + +test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" + +test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_SUB_JWT}\"" + +test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_EMAIL_JWT}\"" + +test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" + +docker stop ${CONTAINER_ID} > /dev/null From 2958537d7f7f963c36c9c7b71a8ba88de5e29c46 Mon Sep 17 00:00:00 2001 From: Maxime Date: Mon, 4 Jun 2018 23:23:55 +0200 Subject: [PATCH 041/130] Using local for test function arguments --- test.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test.sh b/test.sh index d4c8b62..30df5b8 100755 --- a/test.sh +++ b/test.sh @@ -25,10 +25,10 @@ MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctd VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ test_jwt () { - name=$1 - path=$2 - expect=$3 - extra=$4 + local name=$1 + local path=$2 + local expect=$3 + local extra=$4 cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://$MACHINE_IP:8000$path -H 'cache-control: no-cache' $extra" From 15a878aad71d337af85962361e053698031c3917 Mon Sep 17 00:00:00 2001 From: Maxime Date: Mon, 4 Jun 2018 23:37:57 +0200 Subject: [PATCH 042/130] Removed set -e for test Removed set -e for test to contine tests even when one fails --- test.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/test.sh b/test.sh index 30df5b8..0dc9072 100755 --- a/test.sh +++ b/test.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -set -e - RED='\033[01;31m' GREEN='\033[01;32m' NONE='\033[00m' From 5ed9041165e01f537c3174d150bc61bb2cd2eba1 Mon Sep 17 00:00:00 2001 From: Maxime Date: Fri, 8 Jun 2018 21:23:08 +0200 Subject: [PATCH 043/130] Reverted OS selection block in tests --- test.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test.sh b/test.sh index 0dc9072..c37d4de 100755 --- a/test.sh +++ b/test.sh @@ -7,12 +7,8 @@ NONE='\033[00m' DOCKER_IMAGE_NAME=jwt-nginx CONTAINER_ID=$(docker run --rm --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${DOCKER_IMAGE_NAME}) -if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "linux"* ]]; then - # Mac OSX / Linux - MACHINE_IP='localhost' -else - # Windows - MACHINE_IP=`docker-machine ip 2> /dev/null` +if ! MACHINE_IP=`docker-machine ip 2>/dev/null`; then + MACHINE_IP='0.0.0.0' # fix for MacOS fi docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . From ae57c6ba216239f1c3cfdb1fcadbdbb1441e95c2 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sat, 9 Jun 2018 14:17:34 -0400 Subject: [PATCH 044/130] Dockerize tests --- .gitignore | 1 + Dockerfile | 123 +++++++++++++++++++++++++++++++----------------- Dockerfile-test | 4 ++ Makefile | 53 +++++++++++++++++++++ README.md | 63 ++++++++++++++++++------- build.sh | 17 ------- test.sh | 43 +++++++---------- 7 files changed, 201 insertions(+), 103 deletions(-) create mode 100644 Dockerfile-test create mode 100644 Makefile delete mode 100755 build.sh diff --git a/.gitignore b/.gitignore index a51369c..95ee3bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +.idea ngx_http_auth_jwt_module.so diff --git a/Dockerfile b/Dockerfile index 587c6a1..29f7295 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,9 @@ ENV LD_LIBRARY_PATH=/usr/local/lib RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ yum -y update && \ - yum -y groupinstall 'Development Tools' && \ - yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake check-devel check && \ - yum -y install nginx-$NGINX_VERSION + yum -y groupinstall 'Development Tools' && \ + yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake check-devel check && \ + yum -y install nginx-$NGINX_VERSION # for compiling for rh-nginx110 # yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed @@ -24,40 +24,26 @@ WORKDIR /root/dl # build jansson ARG JANSSON_VERSION=2.10 RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ - unzip v$JANSSON_VERSION.zip && \ - rm v$JANSSON_VERSION.zip && \ - ln -sf jansson-$JANSSON_VERSION jansson && \ - cd /root/dl/jansson && \ - cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ - make && \ - make check && \ - make install + unzip v$JANSSON_VERSION.zip && \ + rm v$JANSSON_VERSION.zip && \ + ln -sf jansson-$JANSSON_VERSION jansson && \ + cd /root/dl/jansson && \ + cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ + make && \ + make check && \ + make install # build libjwt ARG LIBJWT_VERSION=1.9.0 RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ - unzip v$LIBJWT_VERSION.zip && \ - rm v$LIBJWT_VERSION.zip && \ - ln -sf libjwt-$LIBJWT_VERSION libjwt && \ - cd /root/dl/libjwt && \ - autoreconf -i && \ - ./configure JANSSON_CFLAGS=/usr/local/include JANSSON_LIBS=/usr/local/lib && \ - make all && \ - make install - -# get our JWT module -# change this to get a specific version? -#ARG TESLA_REPO_NAME=ngx-http-auth-jwt-module -# ARG TESLA_REPO_URL_PREFIX=joefitz/ -# ARG TESLA_REPO_FILE_PREFIX=joefitz- -# ARG TESLA_REPO_FILENAME=validate-authorization-header -#ARG TESLA_REPO_URL_PREFIX= -#ARG TESLA_REPO_FILE_PREFIX= -#ARG TESLA_REPO_FILENAME=master -#ADD https://github.com/TeslaGov/$TESLA_REPO_NAME/archive/${TESLA_REPO_URL_PREFIX}${TESLA_REPO_FILENAME}.zip . -#RUN unzip ${TESLA_REPO_FILENAME}.zip && \ -# rm ${TESLA_REPO_FILENAME}.zip && \ -# ln -sf ${TESLA_REPO_NAME}-${TESLA_REPO_FILE_PREFIX}${TESLA_REPO_FILENAME} ${TESLA_REPO_NAME} + unzip v$LIBJWT_VERSION.zip && \ + rm v$LIBJWT_VERSION.zip && \ + ln -sf libjwt-$LIBJWT_VERSION libjwt && \ + cd /root/dl/libjwt && \ + autoreconf -i && \ + ./configure JANSSON_CFLAGS=/usr/local/include JANSSON_LIBS=/usr/local/lib && \ + make all && \ + make install ADD . /root/dl/ngx-http-auth-jwt-module @@ -76,24 +62,73 @@ ADD . /root/dl/ngx-http-auth-jwt-module # ./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' # #RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ -# tar -xzf nginx-$NGINX_VERSION.tar.gz && \ -# rm nginx-$NGINX_VERSION.tar.gz && \ -# ln -sf nginx-$NGINX_VERSION nginx && \ -# cd /root/dl/nginx && \ +# tar -xzf nginx-$NGINX_VERSION.tar.gz && \ +# rm nginx-$NGINX_VERSION.tar.gz && \ +# ln -sf nginx-$NGINX_VERSION nginx && \ +# cd /root/dl/nginx && \ # ./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ -# make modules && \ -# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. +# make modules && \ +# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. # ARG CACHEBUST=1 RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ - tar -xzf nginx-$NGINX_VERSION.tar.gz && \ - rm nginx-$NGINX_VERSION.tar.gz && \ - ln -sf nginx-$NGINX_VERSION nginx && \ + tar -xzf nginx-$NGINX_VERSION.tar.gz && \ + rm nginx-$NGINX_VERSION.tar.gz && \ + ln -sf nginx-$NGINX_VERSION nginx && \ cd /root/dl/nginx && \ - ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ + ./configure \ + --add-dynamic-module=../ngx-http-auth-jwt-module \ + --prefix=/usr/share/nginx \ + --sbin-path=/usr/sbin/nginx \ + --modules-path=/usr/lib64/nginx/modules \ + --conf-path=/etc/nginx/nginx.conf \ + --error-log-path=/var/log/nginx/error.log \ + --http-log-path=/var/log/nginx/access.log \ + --http-client-body-temp-path=/var/lib/nginx/tmp/client_body \ + --http-proxy-temp-path=/var/lib/nginx/tmp/proxy \ + --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi \ + --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi \ + --http-scgi-temp-path=/var/lib/nginx/tmp/scgi \ + --pid-path=/run/nginx.pid \ + --lock-path=/run/lock/subsys/nginx \ + --user=nginx \ + --group=nginx \ + --with-file-aio \ + --with-ipv6 \ + --with-http_ssl_module \ + --with-http_v2_module \ + --with-http_realip_module \ + --with-http_addition_module \ + --with-http_xslt_module=dynamic \ + --with-http_image_filter_module=dynamic \ + --with-http_geoip_module=dynamic \ + --with-http_sub_module \ + --with-http_dav_module \ + --with-http_flv_module \ + --with-http_mp4_module \ + --with-http_gunzip_module \ + --with-http_gzip_static_module \ + --with-http_random_index_module \ + --with-http_secure_link_module \ + --with-http_degradation_module \ + --with-http_slice_module \ + --with-http_stub_status_module \ + --with-http_perl_module=dynamic \ + --with-mail=dynamic \ + --with-mail_ssl_module \ + --with-pcre \ + --with-pcre-jit \ + --with-stream=dynamic \ + --with-stream_ssl_module \ + --with-google_perftools_module \ + --with-debug \ + --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' \ + --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ make modules && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. + cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. && \ + mkdir /build && \ + cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /build. # Get nginx ready to run COPY resources/nginx.conf /etc/nginx/nginx.conf diff --git a/Dockerfile-test b/Dockerfile-test new file mode 100644 index 0000000..adff57a --- /dev/null +++ b/Dockerfile-test @@ -0,0 +1,4 @@ +FROM alpine:3.7 +RUN apk add --no-cache bash curl +COPY test.sh . +CMD ["./test.sh"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..25a0150 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +SHELL += -eu + +BLUE := \033[0;34m +GREEN := \033[0;32m +RED := \033[0;31m +NC := \033[0m + +DOCKER_ORG_NAME = teslagov +DOCKER_IMAGE_NAME = jwt-nginx + +.PHONY: all +all: + @$(MAKE) build-nginx + @$(MAKE) build-test-runner + @$(MAKE) start-nginx + @$(MAKE) test + +.PHONY: build-nginx +build-nginx: + @echo "${BLUE} Building...${NC}" + @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . ; \ + if [ $$? -ne 0 ] ; \ + then echo "${RED} Build failed :(${NC}" ; \ + else echo "${GREEN}✓ Successfully built NGINX module ${NC}" ; fi + +.PHONY: rebuild-nginx +rebuild-nginx: + @echo "${BLUE} Rebuilding...${NC}" + @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --no-cache ; \ + if [ $$? -ne 0 ] ; \ + then echo "${RED} Build failed :(${NC}" ; \ + else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}" ; fi + +.PHONY: stop-nginx +stop-nginx: + docker stop $(shell docker inspect --format="{{.Id}}" "$(DOCKER_IMAGE_NAME)-cont") ||: + +.PHONY: start-nginx +start-nginx: + docker run --rm --name "$(DOCKER_IMAGE_NAME)-cont" -d -p 8000:8000 $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . + +.PHONY: build-test-runner +build-test-runner: + docker image build -f Dockerfile-test -t $(DOCKER_ORG_NAME)/jwt-nginx-test-runner . + +.PHONY: frebuild-test-runner +rebuild-test-runner: + docker image build -f Dockerfile-test -t $(DOCKER_ORG_NAME)/jwt-nginx-test-runner . --no-cache + +.PHONY: test +test: + docker run --rm $(DOCKER_ORG_NAME)/jwt-nginx-test-runner \ No newline at end of file diff --git a/README.md b/README.md index 7de9088..44b59d4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,43 @@ # Intro This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. -# Build Requirements +## Building and testing +To build the Docker image, start NGINX, and run our Bash test against it, run +```bash +make +``` + +When you make a change to the module, run `make rebuild-nginx`. + +When you make a change to `test.sh`, run `make rebuild-test-runner`. + +| Command | Description | +| -------------------------- |:-------------------------------------------:| +| `make build-nginx` | Builds the NGINX image | +| `make rebuild-nginx` | Re-builds the NGINX image | +| `make build-test-runner` | Builds the image that will run `test.sh` | +| `make rebuild-test-runner` | Re-builds the image that will run `test.sh` | +| `make start-nginx` | Starts the NGINX container | +| `make stop-nginx` | Stops the NGINX container | +| `make test` | Runs `test.sh` against the NGINX container | + +You can re-run tests as many times as you like while NGINX is up. +When you're done running tests, make sure to stop the NGINX container. + +The Dockerfile builds all of the dependencies as well as the module, +downloads a binary version of NGINX, and runs the module as a dynamic module. + +Tests get executed in containers. This project is 100% Docker-ized. + +## Dependencies This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt) -Transitively, that library depends on a JSON Parser called [Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. +Transitively, that library depends on a JSON Parser called +[Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. -# NGINX Directives -This module requires several new nginx.conf directives, which can be specified in on the `main` `server` or `location` level. +## NGINX Directives +This module requires several new `nginx.conf` directives, +which can be specified in on the `main` `server` or `location` level. ``` auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; @@ -17,7 +47,9 @@ auth_jwt_algorithm HS256; # or RS256 auth_jwt_validate_email on; # or off ``` -So, a typical use would be to specify the key and loginurl on the main level and then only turn on the locations that you want to secure (not the login page). Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. +So, a typical use would be to specify the key and loginurl on the main level +and then only turn on the locations that you want to secure (not the login page). +Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. ``` auth_jwt_redirect off; @@ -28,13 +60,16 @@ If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. auth_jwt_validation_type AUTHORIZATION; auth_jwt_validation_type COOKIE=rampartjwt; ``` -By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. +By default the authorization header is used to provide a JWT for validation. +However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. -The default algorithm is 'HS256', for symmetric key validation. Also supported is 'RS256', for RSA 256-bit public key validation. +The default algorithm is 'HS256', for symmetric key validation. +Also supported is 'RS256', for RSA 256-bit public key validation. -If using "auth_jwt_algorithm RS256;", then the 'auth_jwt_key' field must be set to your public key. That is the public key, rather than a PEM certificate. I.e.: +If using "auth_jwt_algorithm RS256;", then the 'auth_jwt_key' field must be set to your public key. +That is the public key, rather than a PEM certificate. I.e.: ``` auth_jwt_key "-----BEGIN PUBLIC KEY----- @@ -48,16 +83,10 @@ oQIDAQAB -----END PUBLIC KEY-----"; ``` - - -By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different user identifier property such as 'sub', set: +By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the +session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different +user identifier property such as 'sub', set: ``` auth_jwt_validate_email off; ``` - - - -The Dockerfile builds all of the dependencies as well as the module, downloads a binary version of nginx, and runs the module as a dynamic module. - -Have a look at build.sh, which creates the docker image and container and executes some test requests to illustrate that some pages are secured by the module and requre a valid JWT. diff --git a/build.sh b/build.sh deleted file mode 100755 index 44c5734..0000000 --- a/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -e - -RED='\033[01;31m' -NONE='\033[00m' - -# build -DOCKER_IMAGE_NAME=jwt-nginx -docker build -t ${DOCKER_IMAGE_NAME} . -if [ $? -ne 0 ] -then - echo -e "${RED}Build Failed${NONE}"; - exit 1; -fi - -./test.sh diff --git a/test.sh b/test.sh index c37d4de..955bb13 100755 --- a/test.sh +++ b/test.sh @@ -4,27 +4,13 @@ RED='\033[01;31m' GREEN='\033[01;32m' NONE='\033[00m' -DOCKER_IMAGE_NAME=jwt-nginx -CONTAINER_ID=$(docker run --rm --name "${DOCKER_IMAGE_NAME}-cont" -d -p 8000:8000 ${DOCKER_IMAGE_NAME}) - -if ! MACHINE_IP=`docker-machine ip 2>/dev/null`; then - MACHINE_IP='0.0.0.0' # fix for MacOS -fi - -docker cp ${CONTAINER_ID}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . - -VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 -MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q -MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM -VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ - test_jwt () { local name=$1 local path=$2 local expect=$3 local extra=$4 - cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://$MACHINE_IP:8000$path -H 'cache-control: no-cache' $extra" + cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://host.docker.internal:8000$path -H 'cache-control: no-cache' $extra" test=$( eval ${cmd} ) if [ "$test" -eq "$expect" ];then @@ -34,22 +20,29 @@ test_jwt () { fi } -test_jwt "Insecure test" "/" "200" +main() { + local VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 + local MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q + local MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM + local VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ + + test_jwt "Insecure test" "/" "200" -test_jwt "Secure test without jwt cookie" "/secure/" "302" + test_jwt "Secure test without jwt cookie" "/secure/" "302" -test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"rampartjwt=${VALIDJWT}\"" + test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"rampartjwt=${VALIDJWT}\"" -test_jwt "Secure test with jwt auth header" "/secure-auth-header/" "200" "--header \"Authorization: Bearer ${VALIDJWT}\"" + test_jwt "Secure test with jwt auth header" "/secure-auth-header/" "200" "--header \"Authorization: Bearer ${VALIDJWT}\"" -test_jwt "Secure test without jwt auth header" "/secure-auth-header/" "302" + test_jwt "Secure test without jwt auth header" "/secure-auth-header/" "302" -test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" + test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" -test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_SUB_JWT}\"" + test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_SUB_JWT}\"" -test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_EMAIL_JWT}\"" + test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_EMAIL_JWT}\"" -test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" + test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" +} -docker stop ${CONTAINER_ID} > /dev/null +main "$@" \ No newline at end of file From 4ea3dcb582a48b231fa2b497220226014525a7d0 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 7 Jan 2019 11:45:31 -0500 Subject: [PATCH 045/130] Update README.md --- README.md | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 44b59d4..8a98a93 100644 --- a/README.md +++ b/README.md @@ -47,28 +47,9 @@ auth_jwt_algorithm HS256; # or RS256 auth_jwt_validate_email on; # or off ``` -So, a typical use would be to specify the key and loginurl on the main level -and then only turn on the locations that you want to secure (not the login page). -Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. - -``` -auth_jwt_redirect off; -``` -If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. - -``` -auth_jwt_validation_type AUTHORIZATION; -auth_jwt_validation_type COOKIE=rampartjwt; -``` -By default the authorization header is used to provide a JWT for validation. -However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. +The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It should represent 256 bits of data and so it should be represented by 32 pairs of hex characters which is 64 characters in total as in the example above. - - -The default algorithm is 'HS256', for symmetric key validation. -Also supported is 'RS256', for RSA 256-bit public key validation. - -If using "auth_jwt_algorithm RS256;", then the 'auth_jwt_key' field must be set to your public key. +The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key. That is the public key, rather than a PEM certificate. I.e.: ``` @@ -83,10 +64,25 @@ oQIDAQAB -----END PUBLIC KEY-----"; ``` -By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the -session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different -user identifier property such as 'sub', set: +A typical use would be to specify the key and loginurl on the main level +and then only turn on the locations that you want to secure (not the login page). +Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. + +``` +auth_jwt_redirect off; +``` +If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. + +``` +auth_jwt_validation_type AUTHORIZATION; +auth_jwt_validation_type COOKIE=rampartjwt; +``` +By default the authorization header is used to provide a JWT for validation. +However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. ``` auth_jwt_validate_email off; ``` +By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the +session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different +user identifier property such as 'sub', set `auth_jwt_validate_email` to the value `off`. From ca93a9355d5ff68e2f9dfbd3d5ba5d9b25d2ad31 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 7 Jan 2019 11:46:15 -0500 Subject: [PATCH 046/130] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a98a93..2bf5f60 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This module requires several new `nginx.conf` directives, which can be specified in on the `main` `server` or `location` level. ``` -auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; +auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; # see docs for format based on algorithm auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; auth_jwt_algorithm HS256; # or RS256 From 80d89d91e3f58d3106af7174d83524d37ddecdc7 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 8 Jan 2019 11:39:25 -0500 Subject: [PATCH 047/130] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bf5f60..c04a8fd 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ auth_jwt_algorithm HS256; # or RS256 auth_jwt_validate_email on; # or off ``` -The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It should represent 256 bits of data and so it should be represented by 32 pairs of hex characters which is 64 characters in total as in the example above. +The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms, Section 5.3.2 The HMAC Key. The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key. That is the public key, rather than a PEM certificate. I.e.: From bf24cbe3d06936ccb16e9d1c7aeab5c84005aa15 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Thu, 2 Jul 2020 17:05:57 -0400 Subject: [PATCH 048/130] Works for NGINX version 1.16.1 in EPEL --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 29f7295..ce3156d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM centos:7 LABEL maintainer="TeslaGov" email="developers@teslagov.com" -ARG NGINX_VERSION=1.12.2 +ARG NGINX_VERSION=1.16.1 ENV LD_LIBRARY_PATH=/usr/local/lib @@ -133,6 +133,8 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ # Get nginx ready to run COPY resources/nginx.conf /etc/nginx/nginx.conf COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf +RUN rm -rf /usr/share/nginx/html +RUN cp -r /root/dl/nginx-1.16.1/html /usr/share/nginx RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header From 820b1a42fab030784d2bcf4b7a052ed90621b82d Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 6 Jul 2020 17:32:06 -0400 Subject: [PATCH 049/130] Update for EPEL7 nginx 1.16.1, updated jansson, updated libjwt --- Dockerfile | 79 +++++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/Dockerfile b/Dockerfile index ce3156d..898cfe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,11 +18,24 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n # for compiling for epel7 RUN yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed geoip geoip-devel google-perftools google-perftools-devel +# Jansson update requires new cmake +RUN yum -y install cmake3 && \ + alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake 10 \ +--slave /usr/local/bin/ctest ctest /usr/bin/ctest \ +--slave /usr/local/bin/cpack cpack /usr/bin/cpack \ +--slave /usr/local/bin/ccmake ccmake /usr/bin/ccmake \ +--family cmake && \ + alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake3 20 \ +--slave /usr/local/bin/ctest ctest /usr/bin/ctest3 \ +--slave /usr/local/bin/cpack cpack /usr/bin/cpack3 \ +--slave /usr/local/bin/ccmake ccmake /usr/bin/ccmake3 \ +--family cmake + RUN mkdir -p /root/dl WORKDIR /root/dl # build jansson -ARG JANSSON_VERSION=2.10 +ARG JANSSON_VERSION=2.13.1 RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ unzip v$JANSSON_VERSION.zip && \ rm v$JANSSON_VERSION.zip && \ @@ -34,7 +47,7 @@ RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ make install # build libjwt -ARG LIBJWT_VERSION=1.9.0 +ARG LIBJWT_VERSION=1.11.0 RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ unzip v$LIBJWT_VERSION.zip && \ rm v$LIBJWT_VERSION.zip && \ @@ -58,8 +71,13 @@ ADD . /root/dl/ngx-http-auth-jwt-module # rh-nginx110 uses these config flags # ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/opt/rh/rh-nginx110/root/usr/share/nginx --sbin-path=/opt/rh/rh-nginx110/root/usr/sbin/nginx --modules-path=/opt/rh/rh-nginx110/root/usr/lib64/nginx/modules --conf-path=/etc/opt/rh/rh-nginx110/nginx/nginx.conf --error-log-path=/var/opt/rh/rh-nginx110/log/nginx/error.log --http-log-path=/var/opt/rh/rh-nginx110/log/nginx/access.log --http-client-body-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/scgi --pid-path=/var/opt/rh/rh-nginx110/run/nginx/nginx.pid --lock-path=/var/opt/rh/rh-nginx110/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=c99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' # -# epel7 version uses these config flags -# ./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' +# epel7 version 1.12.1 uses these config flags +# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' + +# +# epel7 version 1.16.1 uses these config flags +# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' + # #RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ # tar -xzf nginx-$NGINX_VERSION.tar.gz && \ @@ -77,64 +95,17 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ rm nginx-$NGINX_VERSION.tar.gz && \ ln -sf nginx-$NGINX_VERSION nginx && \ cd /root/dl/nginx && \ - ./configure \ - --add-dynamic-module=../ngx-http-auth-jwt-module \ - --prefix=/usr/share/nginx \ - --sbin-path=/usr/sbin/nginx \ - --modules-path=/usr/lib64/nginx/modules \ - --conf-path=/etc/nginx/nginx.conf \ - --error-log-path=/var/log/nginx/error.log \ - --http-log-path=/var/log/nginx/access.log \ - --http-client-body-temp-path=/var/lib/nginx/tmp/client_body \ - --http-proxy-temp-path=/var/lib/nginx/tmp/proxy \ - --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi \ - --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi \ - --http-scgi-temp-path=/var/lib/nginx/tmp/scgi \ - --pid-path=/run/nginx.pid \ - --lock-path=/run/lock/subsys/nginx \ - --user=nginx \ - --group=nginx \ - --with-file-aio \ - --with-ipv6 \ - --with-http_ssl_module \ - --with-http_v2_module \ - --with-http_realip_module \ - --with-http_addition_module \ - --with-http_xslt_module=dynamic \ - --with-http_image_filter_module=dynamic \ - --with-http_geoip_module=dynamic \ - --with-http_sub_module \ - --with-http_dav_module \ - --with-http_flv_module \ - --with-http_mp4_module \ - --with-http_gunzip_module \ - --with-http_gzip_static_module \ - --with-http_random_index_module \ - --with-http_secure_link_module \ - --with-http_degradation_module \ - --with-http_slice_module \ - --with-http_stub_status_module \ - --with-http_perl_module=dynamic \ - --with-mail=dynamic \ - --with-mail_ssl_module \ - --with-pcre \ - --with-pcre-jit \ - --with-stream=dynamic \ - --with-stream_ssl_module \ - --with-google_perftools_module \ - --with-debug \ - --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' \ - --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ + ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ make modules && \ cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. && \ mkdir /build && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /build. + cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /build/. # Get nginx ready to run COPY resources/nginx.conf /etc/nginx/nginx.conf COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf RUN rm -rf /usr/share/nginx/html -RUN cp -r /root/dl/nginx-1.16.1/html /usr/share/nginx +RUN cp -r /root/dl/nginx/html /usr/share/nginx RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header From 638438123811551f433629a0f3acf8ac90dd5ad2 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 7 Jul 2020 00:58:01 -0400 Subject: [PATCH 050/130] upgrade libjwt, copy out all binaries --- .gitignore | 5 +++++ Dockerfile | 7 +++++-- Makefile | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 95ee3bc..8b6727d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea ngx_http_auth_jwt_module.so +libjwt.so.0.6.0 +libjwt.la +libjwt.a +libjansson.so.4.13.0 +libjwt.so.0.7.0 diff --git a/Dockerfile b/Dockerfile index 898cfe9..9808ac3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ LABEL maintainer="TeslaGov" email="developers@teslagov.com" ARG NGINX_VERSION=1.16.1 ENV LD_LIBRARY_PATH=/usr/local/lib +# ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ yum -y update && \ @@ -46,15 +47,17 @@ RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ make check && \ make install +ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig + # build libjwt -ARG LIBJWT_VERSION=1.11.0 +ARG LIBJWT_VERSION=1.12.0 RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ unzip v$LIBJWT_VERSION.zip && \ rm v$LIBJWT_VERSION.zip && \ ln -sf libjwt-$LIBJWT_VERSION libjwt && \ cd /root/dl/libjwt && \ autoreconf -i && \ - ./configure JANSSON_CFLAGS=/usr/local/include JANSSON_LIBS=/usr/local/lib && \ + ./configure && \ make all && \ make install diff --git a/Makefile b/Makefile index 25a0150..3f70dab 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,10 @@ stop-nginx: start-nginx: docker run --rm --name "$(DOCKER_IMAGE_NAME)-cont" -d -p 8000:8000 $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjansson.so.4.13.0 . + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.a . + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.la . + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.so.0.7.0 . .PHONY: build-test-runner build-test-runner: @@ -50,4 +54,4 @@ rebuild-test-runner: .PHONY: test test: - docker run --rm $(DOCKER_ORG_NAME)/jwt-nginx-test-runner \ No newline at end of file + docker run --rm $(DOCKER_ORG_NAME)/jwt-nginx-test-runner From 02c4a990a76aaf8bb0bcff91fb9c17c073da040f Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 7 Jul 2020 10:29:04 -0400 Subject: [PATCH 051/130] rearrange dockerfile --- Dockerfile | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9808ac3..1cd3f5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,11 @@ FROM centos:7 LABEL maintainer="TeslaGov" email="developers@teslagov.com" ARG NGINX_VERSION=1.16.1 +ARG JANSSON_VERSION=2.13.1 +ARG LIBJWT_VERSION=1.12.0 ENV LD_LIBRARY_PATH=/usr/local/lib -# ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig +ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ yum -y update && \ @@ -19,7 +21,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n # for compiling for epel7 RUN yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed geoip geoip-devel google-perftools google-perftools-devel -# Jansson update requires new cmake +# Jansson requires new cmake RUN yum -y install cmake3 && \ alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake 10 \ --slave /usr/local/bin/ctest ctest /usr/bin/ctest \ @@ -36,7 +38,6 @@ RUN mkdir -p /root/dl WORKDIR /root/dl # build jansson -ARG JANSSON_VERSION=2.13.1 RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ unzip v$JANSSON_VERSION.zip && \ rm v$JANSSON_VERSION.zip && \ @@ -47,10 +48,7 @@ RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ make check && \ make install -ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig - # build libjwt -ARG LIBJWT_VERSION=1.12.0 RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ unzip v$LIBJWT_VERSION.zip && \ rm v$LIBJWT_VERSION.zip && \ @@ -63,7 +61,7 @@ RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ ADD . /root/dl/ngx-http-auth-jwt-module -# after 1.11.5 use this command +# after 1.11.5 when compiling for a server that was compiled with --with-compat use this command # ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' # cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /etc/nginx/modules/. # build nginx module against nginx sources @@ -76,21 +74,10 @@ ADD . /root/dl/ngx-http-auth-jwt-module # # epel7 version 1.12.1 uses these config flags # ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' - # # epel7 version 1.16.1 uses these config flags # ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' -# -#RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ -# tar -xzf nginx-$NGINX_VERSION.tar.gz && \ -# rm nginx-$NGINX_VERSION.tar.gz && \ -# ln -sf nginx-$NGINX_VERSION nginx && \ -# cd /root/dl/nginx && \ -# ./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ -# make modules && \ -# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. - # ARG CACHEBUST=1 RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ @@ -100,9 +87,7 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ cd /root/dl/nginx && \ ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ make modules && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. && \ - mkdir /build && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /build/. + cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. # Get nginx ready to run COPY resources/nginx.conf /etc/nginx/nginx.conf @@ -115,6 +100,5 @@ RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect ENTRYPOINT ["/usr/sbin/nginx"] -#ENTRYPOINT ["while true; do echo hello world; sleep 1; done"] -EXPOSE 8000 +EXPOSE 8000 \ No newline at end of file From 3f93751c9439dcb7caa12714052df8b3b7c4815c Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 13 Jul 2020 12:39:52 -0400 Subject: [PATCH 052/130] Copy the PC files out of the docker image --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 3f70dab..8a2290b 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,8 @@ start-nginx: docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.a . docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.la . docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.so.0.7.0 . + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/pkgconfig/jansson.pc . + docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/pkgconfig/libjwt.pc . .PHONY: build-test-runner build-test-runner: From f542086f9df71e2721f576a6f69e1b6f8df0f10a Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 13 Jul 2020 12:40:32 -0400 Subject: [PATCH 053/130] Ignore the PC files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8b6727d..6863045 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ libjwt.la libjwt.a libjansson.so.4.13.0 libjwt.so.0.7.0 +jansson.pc +libjwt.pc From 9908b1b1bc8eaa7564d963311dfdd41a0779a680 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Mon, 13 Jul 2020 12:45:14 -0400 Subject: [PATCH 054/130] Revert "Merge pull request #54 from TeslaGov/pr/42" This reverts commit f6e84522b115829e65f20339fd27bebff0084af3, reversing changes made to bf24cbe3d06936ccb16e9d1c7aeab5c84005aa15. --- README.md | 30 +++--- resources/test-jwt-nginx.conf | 31 ++++--- src/ngx_http_auth_jwt_module.c | 161 +++++++++++++++++++++++++++++---- 3 files changed, 168 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 0349933..c04a8fd 100644 --- a/README.md +++ b/README.md @@ -40,29 +40,13 @@ This module requires several new `nginx.conf` directives, which can be specified in on the `main` `server` or `location` level. ``` -auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; +auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; # see docs for format based on algorithm +auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; auth_jwt_algorithm HS256; # or RS256 auth_jwt_validate_email on; # or off ``` -So, a typical use would be to specify the key on the main level and then only -turn on the locations that you want to secure (not the login page). Unauthorized -requests are given 401 "Unauthorized" responses, you can redirect them with the -nginx's `error_page` directive. - -``` -location @login_redirect { - allow all; - return 302 https://yourdomain.com/loginpage; -} - -location /secure-location/ { - auth_jwt_enabled on; - error_page 401 = @login_redirect; -} -``` - The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms, Section 5.3.2 The HMAC Key. The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key. @@ -80,7 +64,15 @@ oQIDAQAB -----END PUBLIC KEY-----"; ``` -This module supports two ways of presenting the token. +A typical use would be to specify the key and loginurl on the main level +and then only turn on the locations that you want to secure (not the login page). +Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. + +``` +auth_jwt_redirect off; +``` +If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. + ``` auth_jwt_validation_type AUTHORIZATION; auth_jwt_validation_type COOKIE=rampartjwt; diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index b0f1f9d..b39eb95 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -1,38 +1,33 @@ server { auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; - set $auth_jwt_login_url "https://teslagov.com"; + auth_jwt_loginurl "https://teslagov.com"; auth_jwt_enabled off; + auth_jwt_redirect on; listen 8000; server_name localhost; - root /usr/share/nginx/html; - index index.html index.htm; - - location @login_redirect { - return 302 $auth_jwt_login_url?redirect=$request_uri&$args; - } - location ~ ^/secure-no-redirect/ { - rewrite "" / break; auth_jwt_enabled on; + auth_jwt_redirect off; + root /usr/share/nginx; + index index.html index.htm; } location ~ ^/secure/ { - rewrite "" / break; auth_jwt_enabled on; auth_jwt_validation_type COOKIE=rampartjwt; - error_page 401 = @login_redirect; + root /usr/share/nginx; + index index.html index.htm; } location ~ ^/secure-auth-header/ { - rewrite "" / break; auth_jwt_enabled on; - error_page 401 = @login_redirect; + root /usr/share/nginx; + index index.html index.htm; } location ~ ^/secure-rs256/ { - rewrite "" / break; auth_jwt_enabled on; auth_jwt_validation_type COOKIE=rampartjwt; auth_jwt_algorithm RS256; @@ -45,7 +40,13 @@ ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t BwIDAQAB -----END PUBLIC KEY-----"; + root /usr/share/nginx; + index index.html index.htm; } - location / {} + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } } + diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 2c49899..a44e3b9 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -19,8 +19,10 @@ #include "ngx_http_auth_jwt_string.h" typedef struct { + ngx_str_t auth_jwt_loginurl; ngx_str_t auth_jwt_key; ngx_flag_t auth_jwt_enabled; + ngx_flag_t auth_jwt_redirect; ngx_str_t auth_jwt_validation_type; ngx_str_t auth_jwt_algorithm; ngx_flag_t auth_jwt_validate_email; @@ -35,6 +37,13 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type); static ngx_command_t ngx_http_auth_jwt_commands[] = { + { ngx_string("auth_jwt_loginurl"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_loginurl), + NULL }, + { ngx_string("auth_jwt_key"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -49,6 +58,13 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_enabled), NULL }, + { ngx_string("auth_jwt_redirect"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_redirect), + NULL }, + { ngx_string("auth_jwt_validation_type"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -110,6 +126,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_str_t useridHeaderName = ngx_string("x-userid"); ngx_str_t emailHeaderName = ngx_string("x-email"); char* jwtCookieValChrPtr; + char* return_url; ngx_http_auth_jwt_loc_conf_t *jwtcf; u_char *keyBinary; jwt_t *jwt = NULL; @@ -123,10 +140,10 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) time_t now; ngx_str_t auth_jwt_algorithm; int keylen; - + jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); - - if (!jwtcf->auth_jwt_enabled) + + if (!jwtcf->auth_jwt_enabled) { return NGX_DECLINED; } @@ -141,9 +158,9 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (jwtCookieValChrPtr == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); - return NGX_HTTP_UNAUTHORIZED; + goto redirect; } - + // convert key from hex to binary, if a symmetric key auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; @@ -154,7 +171,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); - return NGX_HTTP_UNAUTHORIZED; + goto redirect; } } else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) @@ -166,32 +183,32 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) else { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm"); - return NGX_HTTP_UNAUTHORIZED; + goto redirect; } - + // validate the jwt jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, keylen); if (jwtParseReturnCode != 0) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt"); - return NGX_HTTP_UNAUTHORIZED; + goto redirect; } - + // validate the algorithm alg = jwt_get_alg(jwt); if (alg != JWT_ALG_HS256 && alg != JWT_ALG_RS256) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in jwt %d", alg); - return NGX_HTTP_UNAUTHORIZED; + goto redirect; } - + // validate the exp date of the JWT exp = (time_t)jwt_get_grant_int(jwt, "exp"); now = time(NULL); if (exp < now) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt has expired"); - return NGX_HTTP_UNAUTHORIZED; + goto redirect; } // extract the userid @@ -223,6 +240,103 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_free(jwt); return NGX_OK; + + redirect: + + if (jwt) + { + jwt_free(jwt); + } + + r->headers_out.location = ngx_list_push(&r->headers_out.headers); + + if (r->headers_out.location == NULL) + { + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + } + + r->headers_out.location->hash = 1; + r->headers_out.location->key.len = sizeof("Location") - 1; + r->headers_out.location->key.data = (u_char *) "Location"; + + if (r->method == NGX_HTTP_GET) + { + int loginlen; + char * scheme; + ngx_str_t server; + ngx_str_t uri_variable_name = ngx_string("request_uri"); + ngx_int_t uri_variable_hash; + ngx_http_variable_value_t * request_uri_var; + ngx_str_t uri; + ngx_str_t uri_escaped; + uintptr_t escaped_len; + + loginlen = jwtcf->auth_jwt_loginurl.len; + + scheme = (r->connection->ssl) ? "https" : "http"; + server = r->headers_in.server; + + // get the URI + uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); + request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); + + // get the URI + if(request_uri_var && !request_uri_var->not_found && request_uri_var->valid) + { + // ideally we would like the uri with the querystring parameters + uri.data = ngx_palloc(r->pool, request_uri_var->len); + uri.len = request_uri_var->len; + ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); + + // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "found uri with querystring %s", ngx_str_t_to_char_ptr(r->pool, uri)); + } + else + { + // fallback to the querystring without params + uri = r->uri; + + // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "fallback to querystring without params"); + } + + // escape the URI + escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; + uri_escaped.data = ngx_palloc(r->pool, escaped_len); + uri_escaped.len = escaped_len; + ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); + + r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; + return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); + ngx_memcpy(return_url, jwtcf->auth_jwt_loginurl.data, jwtcf->auth_jwt_loginurl.len); + int return_url_idx = jwtcf->auth_jwt_loginurl.len; + ngx_memcpy(return_url+return_url_idx, "?return_url=", sizeof("?return_url=") - 1); + return_url_idx += sizeof("?return_url=") - 1; + ngx_memcpy(return_url+return_url_idx, scheme, strlen(scheme)); + return_url_idx += strlen(scheme); + ngx_memcpy(return_url+return_url_idx, "://", sizeof("://") - 1); + return_url_idx += sizeof("://") - 1; + ngx_memcpy(return_url+return_url_idx, server.data, server.len); + return_url_idx += server.len; + ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); + return_url_idx += uri_escaped.len; + r->headers_out.location->value.data = (u_char *)return_url; + + // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "return_url: %s", ngx_str_t_to_char_ptr(r->pool, r->headers_out.location->value)); + } + else + { + // for non-get requests, redirect to the login page without a return URL + r->headers_out.location->value.len = jwtcf->auth_jwt_loginurl.len; + r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; + } + + if (jwtcf->auth_jwt_redirect) + { + return NGX_HTTP_MOVED_TEMPORARILY; + } + else + { + return NGX_HTTP_UNAUTHORIZED; + } } @@ -234,7 +348,7 @@ static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf) cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); - if (h == NULL) + if (h == NULL) { return NGX_ERROR; } @@ -251,17 +365,18 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) ngx_http_auth_jwt_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_jwt_loc_conf_t)); - if (conf == NULL) + if (conf == NULL) { return NULL; } - + // set the flag to unset conf->auth_jwt_enabled = (ngx_flag_t) -1; + conf->auth_jwt_redirect = (ngx_flag_t) -1; conf->auth_jwt_validate_email = (ngx_flag_t) -1; ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); - + return conf; } @@ -272,16 +387,22 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_http_auth_jwt_loc_conf_t *prev = parent; ngx_http_auth_jwt_loc_conf_t *conf = child; + ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); - - if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) + + if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) { conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; } + if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) + { + conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; + } + return NGX_CONF_OK; } @@ -320,7 +441,7 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) // get the cookie // TODO: the cookie name could be passed in dynamicallly n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &auth_jwt_validation_type, &jwtCookieVal); - if (n != NGX_DECLINED) + if (n != NGX_DECLINED) { jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); } From 891467e26e3b5b38833a6f91da4dd0d2144dd841 Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Thu, 16 Jul 2020 22:38:15 -0400 Subject: [PATCH 055/130] downgrade libjwt and libjansson --- .gitignore | 2 ++ Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6863045..763f224 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ libjansson.so.4.13.0 libjwt.so.0.7.0 jansson.pc libjwt.pc +libjansson.so.4.10.0 +libjwt.so.0.4.0 diff --git a/Dockerfile b/Dockerfile index 1cd3f5d..31f1342 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ FROM centos:7 LABEL maintainer="TeslaGov" email="developers@teslagov.com" ARG NGINX_VERSION=1.16.1 -ARG JANSSON_VERSION=2.13.1 -ARG LIBJWT_VERSION=1.12.0 +ARG JANSSON_VERSION=2.10 +ARG LIBJWT_VERSION=1.9.0 ENV LD_LIBRARY_PATH=/usr/local/lib ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig From 758ff806949763c6fed6dbc23f9d5b19e256e59c Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 24 Aug 2021 17:16:08 -0400 Subject: [PATCH 056/130] Use nginx.org yum repo instead of epel EPEL wasn't carrying the range of versions of NGINX that the official NGINX was carrying, so I changed from EPEL to NGINX. Also, I changed the ./configure to use the "with-compat" option. At the least, it makes the configure command much simpler... and possibly we might be able to use the module without recompiling (not sure). --- Dockerfile | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 31f1342..2097e5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,21 @@ FROM centos:7 LABEL maintainer="TeslaGov" email="developers@teslagov.com" ARG NGINX_VERSION=1.16.1 -ARG JANSSON_VERSION=2.10 -ARG LIBJWT_VERSION=1.9.0 +ARG JANSSON_VERSION=2.13.1 +ARG LIBJWT_VERSION=1.12.0 ENV LD_LIBRARY_PATH=/usr/local/lib ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig -RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ - yum -y update && \ +RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm +RUN echo "" >>/etc/yum.repos.d/nginx.repo +RUN echo "[nginx]" >>/etc/yum.repos.d/nginx.repo +RUN echo "name=nginx repo" >>/etc/yum.repos.d/nginx.repo +RUN echo "baseurl=https://nginx.org/packages/centos/7/x86_64/" >>/etc/yum.repos.d/nginx.repo +RUN echo "gpgcheck=0" >>/etc/yum.repos.d/nginx.repo +RUN echo "enabled=1" >>/etc/yum.repos.d/nginx.repo + +RUN yum -y update && \ yum -y groupinstall 'Development Tools' && \ yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake check-devel check && \ yum -y install nginx-$NGINX_VERSION @@ -85,7 +92,7 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ rm nginx-$NGINX_VERSION.tar.gz && \ ln -sf nginx-$NGINX_VERSION nginx && \ cd /root/dl/nginx && \ - ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ + ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' && \ make modules && \ cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. @@ -101,4 +108,4 @@ RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect ENTRYPOINT ["/usr/sbin/nginx"] -EXPOSE 8000 \ No newline at end of file +EXPOSE 8000 From 59ed4f97cf461401c901e49b0b951d2a39c339fd Mon Sep 17 00:00:00 2001 From: Joseph Fitzgerald Date: Tue, 24 Aug 2021 17:20:32 -0400 Subject: [PATCH 057/130] Fix possible overflow - thanks @eutychus @eutychus contributed this fix that checks to make sure that the Authorization header contains at least "Bearer: " --- src/ngx_http_auth_jwt_module.c | 14 ++++++++++---- test.sh | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index a44e3b9..4395d5e 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -413,6 +413,7 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) char* jwtCookieValChrPtr = NULL; ngx_str_t jwtCookieVal; ngx_int_t n; + ngx_int_t bearer_length; ngx_str_t authorizationHeaderStr; ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "auth_jwt_validation_type.len %d", auth_jwt_validation_type.len); @@ -425,12 +426,17 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) { ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); - authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; - authorizationHeaderStr.len = authorizationHeader->value.len - (sizeof("Bearer ") - 1); + bearer_length = authorizationHeader->value.len - (sizeof("Bearer ") - 1); - jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); + if (bearer_length > 0) + { + authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; + authorizationHeaderStr.len = bearer_length; + + jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); + } } } else if (auth_jwt_validation_type.len > sizeof("COOKIE=") && ngx_strncmp(auth_jwt_validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1)==0) diff --git a/test.sh b/test.sh index 955bb13..5b9faaa 100755 --- a/test.sh +++ b/test.sh @@ -36,6 +36,8 @@ main() { test_jwt "Secure test without jwt auth header" "/secure-auth-header/" "302" + test_jwt "Secure test with jwt auth header missing Bearer" "/secure-no-redirect/" "401" "--header \"Authorization: X\"" + test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_SUB_JWT}\"" @@ -45,4 +47,4 @@ main() { test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" } -main "$@" \ No newline at end of file +main "$@" From 1653ef1c34a3c18077e137b064da85143f667438 Mon Sep 17 00:00:00 2001 From: Branimir Malesevic Date: Wed, 25 Aug 2021 18:37:40 +0200 Subject: [PATCH 058/130] PEM key file support (#56) * Fix newlines (sry win) in config * Add reading key file name, minify docker image size * Add key file path, minor name change * Fix memory leak * Fix indentation * Add RS256 key file documentation * Fix tests and config * Fix newlines * Add private keyfile fields inside location config * Add tests for rsa256 keyfile scenarios * Update readme Co-authored-by: Branimir Malesevic --- Dockerfile | 22 ++------- README.md | 11 ++++- resources/test-jwt-nginx.conf | 11 +++++ src/ngx_http_auth_jwt_module.c | 90 ++++++++++++++++++++++++++++++++-- test.sh | 5 ++ 5 files changed, 116 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2097e5e..dd5482d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,28 +19,12 @@ RUN echo "enabled=1" >>/etc/yum.repos.d/nginx.repo RUN yum -y update && \ yum -y groupinstall 'Development Tools' && \ - yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake check-devel check && \ + yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake3 check-devel check && \ yum -y install nginx-$NGINX_VERSION -# for compiling for rh-nginx110 -# yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed - # for compiling for epel7 RUN yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed geoip geoip-devel google-perftools google-perftools-devel -# Jansson requires new cmake -RUN yum -y install cmake3 && \ - alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake 10 \ ---slave /usr/local/bin/ctest ctest /usr/bin/ctest \ ---slave /usr/local/bin/cpack cpack /usr/bin/cpack \ ---slave /usr/local/bin/ccmake ccmake /usr/bin/ccmake \ ---family cmake && \ - alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake3 20 \ ---slave /usr/local/bin/ctest ctest /usr/bin/ctest3 \ ---slave /usr/local/bin/cpack cpack /usr/bin/cpack3 \ ---slave /usr/local/bin/ccmake ccmake /usr/bin/ccmake3 \ ---family cmake - RUN mkdir -p /root/dl WORKDIR /root/dl @@ -50,7 +34,7 @@ RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ rm v$JANSSON_VERSION.zip && \ ln -sf jansson-$JANSSON_VERSION jansson && \ cd /root/dl/jansson && \ - cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ + cmake3 . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ make && \ make check && \ make install @@ -99,12 +83,14 @@ RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ # Get nginx ready to run COPY resources/nginx.conf /etc/nginx/nginx.conf COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf +COPY resources/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf RUN rm -rf /usr/share/nginx/html RUN cp -r /root/dl/nginx/html /usr/share/nginx RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect +RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256-file ENTRYPOINT ["/usr/sbin/nginx"] diff --git a/README.md b/README.md index c04a8fd..97e7d82 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,13 @@ auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; auth_jwt_algorithm HS256; # or RS256 auth_jwt_validate_email on; # or off +auth_jwt_use_keyfile off; # or on +auth_jwt_keyfile_path "/app/pub_key"; ``` The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms, Section 5.3.2 The HMAC Key. -The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key. +The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key **OR** `auth_jwt_use_keyfile` should be set to `on` with the `auth_jwt_keyfile_path` set to the public key path (nginx won't start if the `auth_jwt_use_keyfile` is set to `on` without a keyfile). That is the public key, rather than a PEM certificate. I.e.: ``` @@ -64,6 +66,13 @@ oQIDAQAB -----END PUBLIC KEY-----"; ``` +**OR** + +``` +auth_jwt_use_keyfile on; +auth_jwt_keyfile_path "/etc/nginx/pub_key.pem"; +``` + A typical use would be to specify the key and loginurl on the main level and then only turn on the locations that you want to secure (not the login page). Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index b39eb95..cbb356c 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -44,6 +44,17 @@ BwIDAQAB index index.html index.htm; } + location ~ ^/secure-rs256-file/ { + auth_jwt_enabled on; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_algorithm RS256; + auth_jwt_redirect off; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + root /usr/share/nginx; + index index.html index.htm; + } + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 4395d5e..fbd07ba 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -18,6 +18,8 @@ #include "ngx_http_auth_jwt_binary_converters.h" #include "ngx_http_auth_jwt_string.h" +#include + typedef struct { ngx_str_t auth_jwt_loginurl; ngx_str_t auth_jwt_key; @@ -26,7 +28,10 @@ typedef struct { ngx_str_t auth_jwt_validation_type; ngx_str_t auth_jwt_algorithm; ngx_flag_t auth_jwt_validate_email; - + ngx_str_t auth_jwt_keyfile_path; + ngx_flag_t auth_jwt_use_keyfile; + // Private field for keyfile data + ngx_str_t _auth_jwt_keyfile; } ngx_http_auth_jwt_loc_conf_t; static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf); @@ -86,6 +91,20 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validate_email), NULL }, + { ngx_string("auth_jwt_keyfile_path"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_keyfile_path), + NULL }, + + { ngx_string("auth_jwt_use_keyfile"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_use_keyfile), + NULL }, + ngx_null_command }; @@ -129,6 +148,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) char* return_url; ngx_http_auth_jwt_loc_conf_t *jwtcf; u_char *keyBinary; + // For clearing it later on jwt_t *jwt = NULL; int jwtParseReturnCode; jwt_alg_t alg; @@ -177,8 +197,18 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) { // in this case, 'Binary' is a misnomer, as it is the public key string itself - keyBinary = jwtcf->auth_jwt_key.data; - keylen = jwtcf->auth_jwt_key.len; + if (jwtcf->auth_jwt_use_keyfile == 1) + { + // Set to global variables + // NOTE: check for keyBin == NULL skipped, unnecessary check; nginx should fail to start + keyBinary = (u_char*)jwtcf->_auth_jwt_keyfile.data; + keylen = jwtcf->_auth_jwt_keyfile.len; + } + else + { + keyBinary = jwtcf->auth_jwt_key.data; + keylen = jwtcf->auth_jwt_key.len; + } } else { @@ -239,6 +269,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_free(jwt); + return NGX_OK; redirect: @@ -358,7 +389,6 @@ static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf) return NGX_OK; } - static void * ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) { @@ -374,12 +404,43 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) conf->auth_jwt_enabled = (ngx_flag_t) -1; conf->auth_jwt_redirect = (ngx_flag_t) -1; conf->auth_jwt_validate_email = (ngx_flag_t) -1; + conf->auth_jwt_use_keyfile = (ngx_flag_t) -1; ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); return conf; } +// Loads the RSA256 public key into the location config struct +static ngx_int_t +loadAuthKey(ngx_conf_t *cf, ngx_http_auth_jwt_loc_conf_t* conf) { + FILE *keyFile = fopen((const char*)conf->auth_jwt_keyfile_path.data, "rb"); + + // Check if file exists or is correctly opened + if (keyFile == NULL) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to open pub key file"); + return NGX_ERROR; + } + + // Read file length + fseek(keyFile, 0, SEEK_END); + long keySize = ftell(keyFile); + fseek(keyFile, 0, SEEK_SET); + + if (keySize == 0) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "invalid key file size, check the key file"); + return NGX_ERROR; + } + + conf->_auth_jwt_keyfile.data = ngx_palloc(cf->pool, keySize); + fread(conf->_auth_jwt_keyfile.data, 1, keySize, keyFile); + conf->_auth_jwt_keyfile.len = (int)keySize; + + fclose(keyFile); + return NGX_OK; +} static char * ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) @@ -391,6 +452,7 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); + ngx_conf_merge_str_value(conf->auth_jwt_keyfile_path, prev->auth_jwt_keyfile_path, ""); ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) @@ -403,6 +465,26 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; } + if (conf->auth_jwt_use_keyfile == ((ngx_flag_t) -1)) + { + conf->auth_jwt_use_keyfile = (prev->auth_jwt_use_keyfile == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_use_keyfile; + } + + // If the usage of the keyfile is specified, check if the key_path is also configured + if (conf->auth_jwt_use_keyfile == 1) + { + if (ngx_strcmp(conf->auth_jwt_keyfile_path.data, "") != 0) + { + if (loadAuthKey(cf, conf) != NGX_OK) + return NGX_CONF_ERROR; + } + else + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "auth_jwt_keyfile_path not specified"); + return NGX_CONF_ERROR; + } + } + return NGX_CONF_OK; } diff --git a/test.sh b/test.sh index 5b9faaa..f73a03b 100755 --- a/test.sh +++ b/test.sh @@ -25,6 +25,7 @@ main() { local MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q local MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM local VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ + local INVALID_RSA256_JWT=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 test_jwt "Insecure test" "/" "200" @@ -45,6 +46,10 @@ main() { test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_EMAIL_JWT}\"" test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" + + test_jwt "Secure test rsa256 from file with valid jwt" "/secure-rs256-file/" "200" "--header \"Authorization: Bearer ${VALID_RS256_JWT}\"" + + test_jwt "Secure test rsa256 from file with invalid jwt" "/secure-rs256-file/" "401" "--header \"Authorization: Bearer ${INVALID_RSA256_JWT}\"" } main "$@" From 148987d81d31cf16cfb8ccc391056c6e2528ab39 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 20 May 2022 14:30:02 -0400 Subject: [PATCH 059/130] added NGINX_VERSION to Makefile to allow for overriding --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8a2290b..eb7a014 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ all: .PHONY: build-nginx build-nginx: @echo "${BLUE} Building...${NC}" - @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . ; \ + @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --build-arg NGINX_VERSION=${NGINX_VERSION} ; \ if [ $$? -ne 0 ] ; \ then echo "${RED} Build failed :(${NC}" ; \ else echo "${GREEN}✓ Successfully built NGINX module ${NC}" ; fi @@ -26,7 +26,7 @@ build-nginx: .PHONY: rebuild-nginx rebuild-nginx: @echo "${BLUE} Rebuilding...${NC}" - @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --no-cache ; \ + @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --no-cache --build-arg NGINX_VERSION=${NGINX_VERSION} ; \ if [ $$? -ne 0 ] ; \ then echo "${RED} Build failed :(${NC}" ; \ else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}" ; fi From e959c0c39cf5cb24b9161057ed23940fd1a11346 Mon Sep 17 00:00:00 2001 From: Tim Underhay <15734900+KensingtonTech@users.noreply.github.com> Date: Tue, 7 Jun 2022 20:49:30 -0600 Subject: [PATCH 060/130] Docker / build refactor & update Nginx (#69) * Build with latest nginx version (1.21.6) Image builder now multi-stage Image builder uses debian:bullseye-slim to build module (same as official nginx image's base) Use nginx official image as final image base Now using sed to modify OOTB nginx.conf to load the module. Moved test-runner customisations into separate compose test stack Updated Makefile to use docker compose for tests. * Make org name, image name, compose project name, and nginx configurable via env vars. Clean up dangling stage images during build. * Removed copy of libjansson and libjwt from start-nginx target. Updated Readme. * Bumped default version to 1.22.0. Fix for v1.16.1. Test runner runs correct nginx version. Co-authored-by: Josh McCullough --- .env | 1 + Dockerfile | 127 +++++++++++----------------------------- Dockerfile-test | 4 -- Dockerfile-test-nginx | 10 ++++ Dockerfile-test-runner | 4 ++ Makefile | 46 ++++++++------- README.md | 32 +++++----- docker-compose-test.yml | 20 +++++++ resources/nginx.conf | 26 +++----- test.sh | 3 +- 10 files changed, 121 insertions(+), 152 deletions(-) create mode 100644 .env delete mode 100644 Dockerfile-test create mode 100644 Dockerfile-test-nginx create mode 100644 Dockerfile-test-runner create mode 100644 docker-compose-test.yml diff --git a/.env b/.env new file mode 100644 index 0000000..1bb6443 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=jwt-nginx-test \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index dd5482d..648fbee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,97 +1,40 @@ -FROM centos:7 +ARG NGINX_VERSION=1.22.0 -LABEL maintainer="TeslaGov" email="developers@teslagov.com" - -ARG NGINX_VERSION=1.16.1 -ARG JANSSON_VERSION=2.13.1 -ARG LIBJWT_VERSION=1.12.0 - -ENV LD_LIBRARY_PATH=/usr/local/lib -ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig - -RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -RUN echo "" >>/etc/yum.repos.d/nginx.repo -RUN echo "[nginx]" >>/etc/yum.repos.d/nginx.repo -RUN echo "name=nginx repo" >>/etc/yum.repos.d/nginx.repo -RUN echo "baseurl=https://nginx.org/packages/centos/7/x86_64/" >>/etc/yum.repos.d/nginx.repo -RUN echo "gpgcheck=0" >>/etc/yum.repos.d/nginx.repo -RUN echo "enabled=1" >>/etc/yum.repos.d/nginx.repo - -RUN yum -y update && \ - yum -y groupinstall 'Development Tools' && \ - yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake3 check-devel check && \ - yum -y install nginx-$NGINX_VERSION - -# for compiling for epel7 -RUN yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed geoip geoip-devel google-perftools google-perftools-devel - -RUN mkdir -p /root/dl -WORKDIR /root/dl -# build jansson -RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ - unzip v$JANSSON_VERSION.zip && \ - rm v$JANSSON_VERSION.zip && \ - ln -sf jansson-$JANSSON_VERSION jansson && \ - cd /root/dl/jansson && \ - cmake3 . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ - make && \ - make check && \ - make install +FROM debian:bullseye-slim as BASE_IMAGE +LABEL stage=builder +RUN apt-get update \ + && apt-get install -y curl build-essential -# build libjwt -RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ - unzip v$LIBJWT_VERSION.zip && \ - rm v$LIBJWT_VERSION.zip && \ - ln -sf libjwt-$LIBJWT_VERSION libjwt && \ - cd /root/dl/libjwt && \ - autoreconf -i && \ - ./configure && \ - make all && \ - make install +FROM BASE_IMAGE as BUILD_IMAGE +LABEL stage=builder +ENV LD_LIBRARY_PATH=/usr/local/lib +ARG NGINX_VERSION ADD . /root/dl/ngx-http-auth-jwt-module - -# after 1.11.5 when compiling for a server that was compiled with --with-compat use this command -# ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' -# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /etc/nginx/modules/. -# build nginx module against nginx sources -# -# 1.10.2 from nginx by default use config flags... I had to add the -std=c99 and could not achieve "binary compatibility" -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-file-aio --with-threads --with-ipv6 --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_ssl_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -std=c99' -# -# rh-nginx110 uses these config flags -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/opt/rh/rh-nginx110/root/usr/share/nginx --sbin-path=/opt/rh/rh-nginx110/root/usr/sbin/nginx --modules-path=/opt/rh/rh-nginx110/root/usr/lib64/nginx/modules --conf-path=/etc/opt/rh/rh-nginx110/nginx/nginx.conf --error-log-path=/var/opt/rh/rh-nginx110/log/nginx/error.log --http-log-path=/var/opt/rh/rh-nginx110/log/nginx/access.log --http-client-body-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/scgi --pid-path=/var/opt/rh/rh-nginx110/run/nginx/nginx.pid --lock-path=/var/opt/rh/rh-nginx110/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=c99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' -# -# epel7 version 1.12.1 uses these config flags -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' -# -# epel7 version 1.16.1 uses these config flags -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' - -# ARG CACHEBUST=1 - -RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ - tar -xzf nginx-$NGINX_VERSION.tar.gz && \ - rm nginx-$NGINX_VERSION.tar.gz && \ - ln -sf nginx-$NGINX_VERSION nginx && \ - cd /root/dl/nginx && \ - ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' && \ - make modules && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. - -# Get nginx ready to run -COPY resources/nginx.conf /etc/nginx/nginx.conf -COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf -COPY resources/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf -RUN rm -rf /usr/share/nginx/html -RUN cp -r /root/dl/nginx/html /usr/share/nginx -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256-file - -ENTRYPOINT ["/usr/sbin/nginx"] - -EXPOSE 8000 +RUN set -x \ + && apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev \ + && mkdir -p /root/dl +WORKDIR /root/dl +RUN set -x \ + && curl -O http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz \ + && tar -xzf nginx-$NGINX_VERSION.tar.gz \ + && rm nginx-$NGINX_VERSION.tar.gz \ + && ln -sf nginx-$NGINX_VERSION nginx \ + && cd /root/dl/nginx \ + && ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module \ + && make modules + + +FROM nginx:${NGINX_VERSION} +LABEL stage=builder +RUN apt-get update \ + && apt-get -y install libjansson4 libjwt0 \ + && cd /etc/nginx \ + && cp nginx.conf nginx.conf.orig \ + && sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf + + +LABEL stage= +LABEL maintainer="TeslaGov" email="developers@teslagov.com" +COPY --from=BUILD_IMAGE /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ diff --git a/Dockerfile-test b/Dockerfile-test deleted file mode 100644 index adff57a..0000000 --- a/Dockerfile-test +++ /dev/null @@ -1,4 +0,0 @@ -FROM alpine:3.7 -RUN apk add --no-cache bash curl -COPY test.sh . -CMD ["./test.sh"] \ No newline at end of file diff --git a/Dockerfile-test-nginx b/Dockerfile-test-nginx new file mode 100644 index 0000000..10a1eae --- /dev/null +++ b/Dockerfile-test-nginx @@ -0,0 +1,10 @@ +ARG BASE_IMAGE=teslagov/jwt-nginx:latest + +FROM ${BASE_IMAGE} as NGINX +COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf +COPY resources/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf +RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure \ + && cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 \ + && cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256-file \ + && cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header \ + && cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect diff --git a/Dockerfile-test-runner b/Dockerfile-test-runner new file mode 100644 index 0000000..bd9fc59 --- /dev/null +++ b/Dockerfile-test-runner @@ -0,0 +1,4 @@ +FROM alpine:3.7 +COPY test.sh . +RUN apk add curl bash +CMD ["./test.sh"] diff --git a/Makefile b/Makefile index eb7a014..0661439 100644 --- a/Makefile +++ b/Makefile @@ -5,55 +5,61 @@ GREEN := \033[0;32m RED := \033[0;31m NC := \033[0m -DOCKER_ORG_NAME = teslagov -DOCKER_IMAGE_NAME = jwt-nginx +DOCKER_ORG_NAME ?= teslagov +DOCKER_IMAGE_NAME ?= jwt-nginx +COMPOSE_PROJECT_NAME ?= jwt-nginx-test +NGINX_VERSION ?= 1.22.0 .PHONY: all all: @$(MAKE) build-nginx - @$(MAKE) build-test-runner @$(MAKE) start-nginx @$(MAKE) test .PHONY: build-nginx build-nginx: @echo "${BLUE} Building...${NC}" - @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --build-arg NGINX_VERSION=${NGINX_VERSION} ; \ - if [ $$? -ne 0 ] ; \ + @docker image pull debian:bullseye-slim + @docker image pull nginx:${NGINX_VERSION} + @docker image build -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:latest -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} . ; \ + SUCCESS=$$? ; \ + docker rmi $$(docker images --filter=label=stage=builder --quiet); \ + if [ "$$SUCCESS" -ne 0 ] ; \ then echo "${RED} Build failed :(${NC}" ; \ else echo "${GREEN}✓ Successfully built NGINX module ${NC}" ; fi .PHONY: rebuild-nginx rebuild-nginx: @echo "${BLUE} Rebuilding...${NC}" - @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --no-cache --build-arg NGINX_VERSION=${NGINX_VERSION} ; \ - if [ $$? -ne 0 ] ; \ + @docker image pull debian:bullseye-slim + @docker image pull nginx:${NGINX_VERSION} + @docker image build -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:latest -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} . --no-cache ; \ + SUCCESS=$$? ; \ + docker rmi $$(docker images --filter=label=stage=builder --quiet); \ + if [ "$$SUCCESS" -ne 0 ] ; \ then echo "${RED} Build failed :(${NC}" ; \ else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}" ; fi .PHONY: stop-nginx stop-nginx: - docker stop $(shell docker inspect --format="{{.Id}}" "$(DOCKER_IMAGE_NAME)-cont") ||: + docker stop $(shell docker inspect --format="{{.Id}}" "$(DOCKER_IMAGE_NAME)") ||: .PHONY: start-nginx start-nginx: - docker run --rm --name "$(DOCKER_IMAGE_NAME)-cont" -d -p 8000:8000 $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjansson.so.4.13.0 . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.a . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.la . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.so.0.7.0 . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/pkgconfig/jansson.pc . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/pkgconfig/libjwt.pc . + docker run --rm --name "${DOCKER_IMAGE_NAME}" -d -p 8000:8000 ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME} + docker cp ${DOCKER_IMAGE_NAME}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . .PHONY: build-test-runner build-test-runner: - docker image build -f Dockerfile-test -t $(DOCKER_ORG_NAME)/jwt-nginx-test-runner . + IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml build -.PHONY: frebuild-test-runner +.PHONY: rebuild-test-runner rebuild-test-runner: - docker image build -f Dockerfile-test -t $(DOCKER_ORG_NAME)/jwt-nginx-test-runner . --no-cache + IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml build --no-cache .PHONY: test test: - docker run --rm $(DOCKER_ORG_NAME)/jwt-nginx-test-runner + IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml up --no-start + docker start ${COMPOSE_PROJECT_NAME}-nginx-1 + docker start -a ${COMPOSE_PROJECT_NAME}-runner-1 + docker compose -f ./docker-compose-test.yml down \ No newline at end of file diff --git a/README.md b/README.md index 97e7d82..613ace6 100644 --- a/README.md +++ b/README.md @@ -11,23 +11,21 @@ When you make a change to the module, run `make rebuild-nginx`. When you make a change to `test.sh`, run `make rebuild-test-runner`. -| Command | Description | -| -------------------------- |:-------------------------------------------:| -| `make build-nginx` | Builds the NGINX image | -| `make rebuild-nginx` | Re-builds the NGINX image | -| `make build-test-runner` | Builds the image that will run `test.sh` | -| `make rebuild-test-runner` | Re-builds the image that will run `test.sh` | -| `make start-nginx` | Starts the NGINX container | -| `make stop-nginx` | Stops the NGINX container | -| `make test` | Runs `test.sh` against the NGINX container | - -You can re-run tests as many times as you like while NGINX is up. -When you're done running tests, make sure to stop the NGINX container. - -The Dockerfile builds all of the dependencies as well as the module, -downloads a binary version of NGINX, and runs the module as a dynamic module. - -Tests get executed in containers. This project is 100% Docker-ized. +| Command | Description | +| -------------------------- |:-----------------------------------------------------------------:| +| `make build-nginx` | Builds the NGINX image | +| `make rebuild-nginx` | Re-builds the NGINX image | +| `make build-test-runner` | Builds the images used by the test stack (uses Docker compose) | +| `make rebuild-test-runner` | Re-builds the images used by the test stack | +| `make start-nginx` | Starts the NGINX container | +| `make stop-nginx` | Stops the NGINX container | +| `make test` | Runs `test.sh` against the NGINX container (uses Docker compose) | + +The image produced with `make build-nginx` only differs from the official Nginx image in two ways: the module itself and the nginx.conf configuration entry that loads it. + +The tests use a customized Nginx image, distinct from the main image, as well as a test runner image. By running `make test`, the two test containers will be created up with Docker compose, started, and the tests run. At the end, both containers will be automatically stopped and destroyed. + +This project is 100% Docker-ized. ## Dependencies This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt) diff --git a/docker-compose-test.yml b/docker-compose-test.yml new file mode 100644 index 0000000..8d406f2 --- /dev/null +++ b/docker-compose-test.yml @@ -0,0 +1,20 @@ +version: '3.3' + +services: + + nginx: + build: + context: . + dockerfile: Dockerfile-test-nginx + args: + BASE_IMAGE: ${IMAGE_NAME:-teslagov/jwt-nginx}:${IMAGE_VERSION:-latest} + + runner: + build: + context: . + dockerfile: Dockerfile-test-runner + environment: + BASE_IMAGE: ${IMAGE_NAME:-teslagov/jwt-nginx}:${IMAGE_VERSION:-latest} + + depends_on: + - nginx-test \ No newline at end of file diff --git a/resources/nginx.conf b/resources/nginx.conf index 7ea8afb..3981730 100644 --- a/resources/nginx.conf +++ b/resources/nginx.conf @@ -1,12 +1,12 @@ user nginx; -worker_processes 1; +worker_processes auto; -error_log /var/log/nginx/error.log info; +error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; - load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; + events { worker_connections 1024; } @@ -16,14 +16,11 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - log_format upstream_time '$remote_addr $sent_http_x_userid [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for" ' - 'rt="$request_time" uct="$upstream_connect_time" ' - 'uht="$upstream_header_time" urt="$upstream_response_time" ' - '$sent_http_x_email'; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log upstream_time; + access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; @@ -32,12 +29,5 @@ http { #gzip on; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Server $remote_addr; - include /etc/nginx/conf.d/*.conf; -} - -daemon off; +} \ No newline at end of file diff --git a/test.sh b/test.sh index f73a03b..adeb777 100755 --- a/test.sh +++ b/test.sh @@ -10,7 +10,8 @@ test_jwt () { local expect=$3 local extra=$4 - cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://host.docker.internal:8000$path -H 'cache-control: no-cache' $extra" + cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://nginx:8000$path -H 'cache-control: no-cache' $extra" + test=$( eval ${cmd} ) if [ "$test" -eq "$expect" ];then From 16ea0fe374176d6f5315dbd85e542fc80d38ac4f Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 7 Jun 2022 23:04:56 -0400 Subject: [PATCH 061/130] Dockerfile formatting --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 648fbee..ad36f02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,9 +30,9 @@ FROM nginx:${NGINX_VERSION} LABEL stage=builder RUN apt-get update \ && apt-get -y install libjansson4 libjwt0 \ - && cd /etc/nginx \ - && cp nginx.conf nginx.conf.orig \ - && sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf + && cd /etc/nginx \ + && cp nginx.conf nginx.conf.orig \ + && sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf LABEL stage= From 9f8991f66a5e334f29a264fb862473556de25489 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 7 Jun 2022 23:05:53 -0400 Subject: [PATCH 062/130] Makefile cleanup --- Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 0661439..252f799 100644 --- a/Makefile +++ b/Makefile @@ -25,24 +25,24 @@ build-nginx: SUCCESS=$$? ; \ docker rmi $$(docker images --filter=label=stage=builder --quiet); \ if [ "$$SUCCESS" -ne 0 ] ; \ - then echo "${RED} Build failed :(${NC}" ; \ - else echo "${GREEN}✓ Successfully built NGINX module ${NC}" ; fi + then echo "${RED} Build failed ${NC}"; \ + else echo "${GREEN}✓ Successfully built NGINX module ${NC}"; fi .PHONY: rebuild-nginx rebuild-nginx: @echo "${BLUE} Rebuilding...${NC}" @docker image pull debian:bullseye-slim @docker image pull nginx:${NGINX_VERSION} - @docker image build -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:latest -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} . --no-cache ; \ + @docker image build -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:latest -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} --no-cache .; \ SUCCESS=$$? ; \ docker rmi $$(docker images --filter=label=stage=builder --quiet); \ if [ "$$SUCCESS" -ne 0 ] ; \ - then echo "${RED} Build failed :(${NC}" ; \ - else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}" ; fi + then echo "${RED} Build failed ${NC}"; \ + else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}"; fi .PHONY: stop-nginx stop-nginx: - docker stop $(shell docker inspect --format="{{.Id}}" "$(DOCKER_IMAGE_NAME)") ||: + docker stop "${DOCKER_IMAGE_NAME}" .PHONY: start-nginx start-nginx: From 60b6f4bd3110a186983498b0810ba7aa8b6f421c Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 7 Jun 2022 23:06:14 -0400 Subject: [PATCH 063/130] add task to copy binaries from container --- .gitignore | 11 +---------- Makefile | 10 +++++++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 763f224..a676215 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,2 @@ .idea -ngx_http_auth_jwt_module.so -libjwt.so.0.6.0 -libjwt.la -libjwt.a -libjansson.so.4.13.0 -libjwt.so.0.7.0 -jansson.pc -libjwt.pc -libjansson.so.4.10.0 -libjwt.so.0.4.0 +bin diff --git a/Makefile b/Makefile index 252f799..4555781 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,15 @@ stop-nginx: .PHONY: start-nginx start-nginx: docker run --rm --name "${DOCKER_IMAGE_NAME}" -d -p 8000:8000 ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME} - docker cp ${DOCKER_IMAGE_NAME}:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . + +.PHONY: cp-bin +cp-bin: start-nginx + rm -rf bin + mkdir -p bin + docker exec jwt-nginx sh -c "tar -chf - \ + /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ + /usr/lib/x86_64-linux-gnu/libjansson.so.* \ + /usr/lib/x86_64-linux-gnu/libjwt.*" 2>/dev/null | tar -xf - -C bin &>/dev/null .PHONY: build-test-runner build-test-runner: From aa024c58163b0639dbc95377c8c4cd95fcbf4cd0 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 8 Jun 2022 10:26:33 -0400 Subject: [PATCH 064/130] add option to not extract sub -- fixes #66 (#70) * add option to not extract sub * move email variables into block where they are used * cleanup --- README.md | 15 ++++++++-- src/ngx_http_auth_jwt_module.c | 55 ++++++++++++++++++++-------------- test.sh | 5 ++-- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 613ace6..718d7b5 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,10 @@ which can be specified in on the `main` `server` or `location` level. auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; # see docs for format based on algorithm auth_jwt_loginurl "https://yourdomain.com/loginpage"; auth_jwt_enabled on; -auth_jwt_algorithm HS256; # or RS256 +auth_jwt_algorithm HS256; # or RS256 +auth_jwt_extract_sub on; # or off auth_jwt_validate_email on; # or off -auth_jwt_use_keyfile off; # or on +auth_jwt_use_keyfile off; # or on auth_jwt_keyfile_path "/app/pub_key"; ``` @@ -87,9 +88,17 @@ auth_jwt_validation_type COOKIE=rampartjwt; By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. +``` +auth_jwt_extract_sub +``` +By default, the module will attempt to extract the `sub` claim (e.g. the user's id) from the JWT. If successful, the +value will be set in the `x-userid` HTTP header. An error will be logged if this option is enabled and the JWT does not +contain the `sub` claim. + ``` auth_jwt_validate_email off; ``` By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different -user identifier property such as 'sub', set `auth_jwt_validate_email` to the value `off`. +user identifier property such as `sub`, set `auth_jwt_validate_email` to the value `off`. _Note that this flag may be +renamed to `auth_jwt_extract_email` in a future release._ diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index fbd07ba..f5ca357 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -27,6 +27,7 @@ typedef struct { ngx_flag_t auth_jwt_redirect; ngx_str_t auth_jwt_validation_type; ngx_str_t auth_jwt_algorithm; + ngx_flag_t auth_jwt_extract_sub; ngx_flag_t auth_jwt_validate_email; ngx_str_t auth_jwt_keyfile_path; ngx_flag_t auth_jwt_use_keyfile; @@ -84,6 +85,13 @@ static ngx_command_t ngx_http_auth_jwt_commands[] = { offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_algorithm), NULL }, + { ngx_string("auth_jwt_extract_sub"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_extract_sub), + NULL }, + { ngx_string("auth_jwt_validate_email"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, @@ -152,10 +160,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_t *jwt = NULL; int jwtParseReturnCode; jwt_alg_t alg; - const char* sub; - const char* email; - ngx_str_t sub_t; - ngx_str_t email_t; time_t exp; time_t now; ngx_str_t auth_jwt_algorithm; @@ -175,6 +179,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } jwtCookieValChrPtr = getJwt(r, jwtcf->auth_jwt_validation_type); + if (jwtCookieValChrPtr == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); @@ -184,6 +189,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // convert key from hex to binary, if a symmetric key auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; + if (auth_jwt_algorithm.len == 0 || (auth_jwt_algorithm.len == sizeof("HS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "HS256", sizeof("HS256") - 1)==0)) { keylen = jwtcf->auth_jwt_key.len / 2; @@ -218,6 +224,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // validate the jwt jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, keylen); + if (jwtParseReturnCode != 0) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt"); @@ -226,6 +233,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // validate the algorithm alg = jwt_get_alg(jwt); + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_RS256) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in jwt %d", alg); @@ -235,6 +243,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) // validate the exp date of the JWT exp = (time_t)jwt_get_grant_int(jwt, "exp"); now = time(NULL); + if (exp < now) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt has expired"); @@ -242,38 +251,43 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } // extract the userid - sub = jwt_get_grant(jwt, "sub"); - if (sub == NULL) + if (jwtcf->auth_jwt_extract_sub == 1) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain a subject"); - } - else - { - sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); - set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); + const char* sub = jwt_get_grant(jwt, "sub"); + + if (sub == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain a subject"); + } + else + { + ngx_str_t sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); + + set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); + } } if (jwtcf->auth_jwt_validate_email == 1) { - email = jwt_get_grant(jwt, "emailAddress"); + const char* email = jwt_get_grant(jwt, "emailAddress"); + if (email == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); } else { - email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); + ngx_str_t email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); + set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); } } jwt_free(jwt); - return NGX_OK; redirect: - if (jwt) { jwt_free(jwt); @@ -303,7 +317,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) uintptr_t escaped_len; loginlen = jwtcf->auth_jwt_loginurl.len; - scheme = (r->connection->ssl) ? "https" : "http"; server = r->headers_in.server; @@ -318,15 +331,11 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) uri.data = ngx_palloc(r->pool, request_uri_var->len); uri.len = request_uri_var->len; ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "found uri with querystring %s", ngx_str_t_to_char_ptr(r->pool, uri)); } else { // fallback to the querystring without params uri = r->uri; - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "fallback to querystring without params"); } // escape the URI @@ -350,8 +359,6 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); return_url_idx += uri_escaped.len; r->headers_out.location->value.data = (u_char *)return_url; - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "return_url: %s", ngx_str_t_to_char_ptr(r->pool, r->headers_out.location->value)); } else { @@ -403,6 +410,7 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) // set the flag to unset conf->auth_jwt_enabled = (ngx_flag_t) -1; conf->auth_jwt_redirect = (ngx_flag_t) -1; + conf->auth_jwt_extract_sub = (ngx_flag_t) -1; conf->auth_jwt_validate_email = (ngx_flag_t) -1; conf->auth_jwt_use_keyfile = (ngx_flag_t) -1; @@ -453,6 +461,7 @@ ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); ngx_conf_merge_str_value(conf->auth_jwt_keyfile_path, prev->auth_jwt_keyfile_path, ""); + ngx_conf_merge_off_value(conf->auth_jwt_extract_sub, prev->auth_jwt_extract_sub, 1); ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) diff --git a/test.sh b/test.sh index adeb777..55230b4 100755 --- a/test.sh +++ b/test.sh @@ -11,10 +11,9 @@ test_jwt () { local extra=$4 cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://nginx:8000$path -H 'cache-control: no-cache' $extra" - - test=$( eval ${cmd} ) - if [ "$test" -eq "$expect" ];then + + if [ "$test" -eq "$expect" ]; then echo -e "${GREEN}${name}: passed (${test})${NONE}"; else echo -e "${RED}${name}: failed (${test})${NONE}"; From 8f39e48fbbeecf909c6c1b7e5dd894b9daee6c4b Mon Sep 17 00:00:00 2001 From: Harm van Tilborg Date: Mon, 15 Aug 2022 15:51:50 +0200 Subject: [PATCH 065/130] Do not respond with a "Location" header when redirects are disabled (#74) * In the default nginx config (used in the Docker container), nginx listens on port 80 * Do not respond with a "Location" header when redirects are disabled --- Makefile | 4 +- src/ngx_http_auth_jwt_module.c | 143 ++++++++++++++++----------------- 2 files changed, 73 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index 4555781..17c7aa2 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ stop-nginx: .PHONY: start-nginx start-nginx: - docker run --rm --name "${DOCKER_IMAGE_NAME}" -d -p 8000:8000 ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME} + docker run --rm --name "${DOCKER_IMAGE_NAME}" -d -p 8000:80 ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME} .PHONY: cp-bin cp-bin: start-nginx @@ -70,4 +70,4 @@ test: IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml up --no-start docker start ${COMPOSE_PROJECT_NAME}-nginx-1 docker start -a ${COMPOSE_PROJECT_NAME}-runner-1 - docker compose -f ./docker-compose-test.yml down \ No newline at end of file + docker compose -f ./docker-compose-test.yml down diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index f5ca357..f91e608 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -227,7 +227,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (jwtParseReturnCode != 0) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt"); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt, error code %d", jwtParseReturnCode); goto redirect; } @@ -293,88 +293,87 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) jwt_free(jwt); } - r->headers_out.location = ngx_list_push(&r->headers_out.headers); - - if (r->headers_out.location == NULL) + if (jwtcf->auth_jwt_redirect) { - ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); - } + r->headers_out.location = ngx_list_push(&r->headers_out.headers); - r->headers_out.location->hash = 1; - r->headers_out.location->key.len = sizeof("Location") - 1; - r->headers_out.location->key.data = (u_char *) "Location"; + if (r->headers_out.location == NULL) + { + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + } - if (r->method == NGX_HTTP_GET) - { - int loginlen; - char * scheme; - ngx_str_t server; - ngx_str_t uri_variable_name = ngx_string("request_uri"); - ngx_int_t uri_variable_hash; - ngx_http_variable_value_t * request_uri_var; - ngx_str_t uri; - ngx_str_t uri_escaped; - uintptr_t escaped_len; - - loginlen = jwtcf->auth_jwt_loginurl.len; - scheme = (r->connection->ssl) ? "https" : "http"; - server = r->headers_in.server; - - // get the URI - uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); - request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); - - // get the URI - if(request_uri_var && !request_uri_var->not_found && request_uri_var->valid) + r->headers_out.location->hash = 1; + r->headers_out.location->key.len = sizeof("Location") - 1; + r->headers_out.location->key.data = (u_char *) "Location"; + + if (r->method == NGX_HTTP_GET) { - // ideally we would like the uri with the querystring parameters - uri.data = ngx_palloc(r->pool, request_uri_var->len); - uri.len = request_uri_var->len; - ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); + int loginlen; + char * scheme; + ngx_str_t server; + ngx_str_t uri_variable_name = ngx_string("request_uri"); + ngx_int_t uri_variable_hash; + ngx_http_variable_value_t * request_uri_var; + ngx_str_t uri; + ngx_str_t uri_escaped; + uintptr_t escaped_len; + + loginlen = jwtcf->auth_jwt_loginurl.len; + scheme = (r->connection->ssl) ? "https" : "http"; + server = r->headers_in.server; + + // get the URI + uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); + request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); + + // get the URI + if(request_uri_var && !request_uri_var->not_found && request_uri_var->valid) + { + // ideally we would like the uri with the querystring parameters + uri.data = ngx_palloc(r->pool, request_uri_var->len); + uri.len = request_uri_var->len; + ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); + } + else + { + // fallback to the querystring without params + uri = r->uri; + } + + // escape the URI + escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; + uri_escaped.data = ngx_palloc(r->pool, escaped_len); + uri_escaped.len = escaped_len; + ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); + + r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; + return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); + ngx_memcpy(return_url, jwtcf->auth_jwt_loginurl.data, jwtcf->auth_jwt_loginurl.len); + int return_url_idx = jwtcf->auth_jwt_loginurl.len; + ngx_memcpy(return_url+return_url_idx, "?return_url=", sizeof("?return_url=") - 1); + return_url_idx += sizeof("?return_url=") - 1; + ngx_memcpy(return_url+return_url_idx, scheme, strlen(scheme)); + return_url_idx += strlen(scheme); + ngx_memcpy(return_url+return_url_idx, "://", sizeof("://") - 1); + return_url_idx += sizeof("://") - 1; + ngx_memcpy(return_url+return_url_idx, server.data, server.len); + return_url_idx += server.len; + ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); + return_url_idx += uri_escaped.len; + r->headers_out.location->value.data = (u_char *)return_url; } else { - // fallback to the querystring without params - uri = r->uri; + // for non-get requests, redirect to the login page without a return URL + r->headers_out.location->value.len = jwtcf->auth_jwt_loginurl.len; + r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; } - // escape the URI - escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; - uri_escaped.data = ngx_palloc(r->pool, escaped_len); - uri_escaped.len = escaped_len; - ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); - - r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; - return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); - ngx_memcpy(return_url, jwtcf->auth_jwt_loginurl.data, jwtcf->auth_jwt_loginurl.len); - int return_url_idx = jwtcf->auth_jwt_loginurl.len; - ngx_memcpy(return_url+return_url_idx, "?return_url=", sizeof("?return_url=") - 1); - return_url_idx += sizeof("?return_url=") - 1; - ngx_memcpy(return_url+return_url_idx, scheme, strlen(scheme)); - return_url_idx += strlen(scheme); - ngx_memcpy(return_url+return_url_idx, "://", sizeof("://") - 1); - return_url_idx += sizeof("://") - 1; - ngx_memcpy(return_url+return_url_idx, server.data, server.len); - return_url_idx += server.len; - ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); - return_url_idx += uri_escaped.len; - r->headers_out.location->value.data = (u_char *)return_url; - } - else - { - // for non-get requests, redirect to the login page without a return URL - r->headers_out.location->value.len = jwtcf->auth_jwt_loginurl.len; - r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; - } - - if (jwtcf->auth_jwt_redirect) - { return NGX_HTTP_MOVED_TEMPORARILY; } - else - { - return NGX_HTTP_UNAUTHORIZED; - } + + // When no redirect is needed, no "Location" header construction is needed, and we can respond with a 401 + return NGX_HTTP_UNAUTHORIZED; } From 1cf8606dd89deb74f275d1e4b81c3d6d6913cefb Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 28 Oct 2022 14:17:34 -0400 Subject: [PATCH 066/130] update cookie name in README and test --- README.md | 2 +- resources/test-jwt-nginx.conf | 4 ++-- test.sh | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 718d7b5..fe1a2e5 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. ``` auth_jwt_validation_type AUTHORIZATION; -auth_jwt_validation_type COOKIE=rampartjwt; +auth_jwt_validation_type COOKIE=jwt; ``` By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf index cbb356c..53f512b 100644 --- a/resources/test-jwt-nginx.conf +++ b/resources/test-jwt-nginx.conf @@ -16,7 +16,7 @@ server { location ~ ^/secure/ { auth_jwt_enabled on; - auth_jwt_validation_type COOKIE=rampartjwt; + auth_jwt_validation_type COOKIE=jwt; root /usr/share/nginx; index index.html index.htm; } @@ -29,7 +29,7 @@ server { location ~ ^/secure-rs256/ { auth_jwt_enabled on; - auth_jwt_validation_type COOKIE=rampartjwt; + auth_jwt_validation_type COOKIE=jwt; auth_jwt_algorithm RS256; auth_jwt_key "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh diff --git a/test.sh b/test.sh index 55230b4..45d21ee 100755 --- a/test.sh +++ b/test.sh @@ -31,7 +31,7 @@ main() { test_jwt "Secure test without jwt cookie" "/secure/" "302" - test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"rampartjwt=${VALIDJWT}\"" + test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"jwt=${VALIDJWT}\"" test_jwt "Secure test with jwt auth header" "/secure-auth-header/" "200" "--header \"Authorization: Bearer ${VALIDJWT}\"" @@ -41,11 +41,11 @@ main() { test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" - test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_SUB_JWT}\"" + test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"jwt=${MISSING_SUB_JWT}\"" - test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_EMAIL_JWT}\"" + test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"jwt=${MISSING_EMAIL_JWT}\"" - test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" + test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"jwt=${VALID_RS256_JWT}\"" test_jwt "Secure test rsa256 from file with valid jwt" "/secure-rs256-file/" "200" "--header \"Authorization: Bearer ${VALID_RS256_JWT}\"" From bd0911851838428a62537f988ffbf165e8d10e18 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 28 Oct 2022 14:28:45 -0400 Subject: [PATCH 067/130] update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fe1a2e5..ab4f8cd 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ This is an NGINX module to check for a valid JWT and proxy to an upstream server ## Building and testing To build the Docker image, start NGINX, and run our Bash test against it, run + ```bash make ``` From 223a4e298c1e423efb98712ba6e887278880466a Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 28 Oct 2022 14:28:50 -0400 Subject: [PATCH 068/130] fix tests --- docker-compose-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 8d406f2..021d191 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -17,4 +17,4 @@ services: BASE_IMAGE: ${IMAGE_NAME:-teslagov/jwt-nginx}:${IMAGE_VERSION:-latest} depends_on: - - nginx-test \ No newline at end of file + - nginx \ No newline at end of file From d1507058a3385421ecb31ac379c7b3e8ec07c3d1 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 7 Nov 2022 21:09:30 -0500 Subject: [PATCH 069/130] rename variable for clarity --- src/ngx_http_auth_jwt_module.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index f91e608..4209e3c 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -152,7 +152,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) { ngx_str_t useridHeaderName = ngx_string("x-userid"); ngx_str_t emailHeaderName = ngx_string("x-email"); - char* jwtCookieValChrPtr; + char* jwtPtr; char* return_url; ngx_http_auth_jwt_loc_conf_t *jwtcf; u_char *keyBinary; @@ -178,9 +178,9 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) return NGX_DECLINED; } - jwtCookieValChrPtr = getJwt(r, jwtcf->auth_jwt_validation_type); + jwtPtr = getJwt(r, jwtcf->auth_jwt_validation_type); - if (jwtCookieValChrPtr == NULL) + if (jwtPtr == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); goto redirect; @@ -223,7 +223,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } // validate the jwt - jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, keylen); + jwtParseReturnCode = jwt_decode(&jwt, jwtPtr, keyBinary, keylen); if (jwtParseReturnCode != 0) { @@ -500,7 +500,7 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) { static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); ngx_table_elt_t *authorizationHeader; - char* jwtCookieValChrPtr = NULL; + char* jwtPtr = NULL; ngx_str_t jwtCookieVal; ngx_int_t n; ngx_int_t bearer_length; @@ -523,9 +523,9 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; authorizationHeaderStr.len = bearer_length; - jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); + jwtPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtPtr); } } } @@ -539,11 +539,11 @@ static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &auth_jwt_validation_type, &jwtCookieVal); if (n != NGX_DECLINED) { - jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); } } - return jwtCookieValChrPtr; + return jwtPtr; } From d7c3cb48a3ec7b11f0e5eda9f36a7b11c7bc2c7d Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 9 Nov 2022 10:25:33 -0500 Subject: [PATCH 070/130] add support for higher-bit HS/RS algorithms (#80) * add support for higher-bit HS/RS algorithms fixes #77 * add Git hooks * use variable for Docker image name * cleanup --- .bin/git/hooks-wrapper | 43 ++++++ .bin/git/hooks/pre-push-build-and-test | 12 ++ .bin/git/init-hooks | 19 +++ .bin/init | 3 + Dockerfile | 35 ++--- Dockerfile-test-nginx | 10 -- Makefile | 73 --------- README.md | 142 ++++++++++++------ config | 7 +- docker-compose-test.yml | 20 --- resources/nginx.conf | 22 +-- resources/test-jwt-nginx.conf | 63 -------- scripts.sh | 89 +++++++++++ src/ngx_http_auth_jwt_module.c | 20 +-- test.sh | 55 ------- test/Dockerfile-test-nginx | 5 + .../Dockerfile-test-runner | 0 test/docker-compose-test.yml | 24 +++ {resources => test}/rsa_key_2048-pub.pem | 0 {resources => test}/rsa_key_2048.pem | 0 test/test.conf | 134 +++++++++++++++++ test/test.sh | 139 +++++++++++++++++ 22 files changed, 598 insertions(+), 317 deletions(-) create mode 100755 .bin/git/hooks-wrapper create mode 100755 .bin/git/hooks/pre-push-build-and-test create mode 100755 .bin/git/init-hooks create mode 100755 .bin/init delete mode 100644 Dockerfile-test-nginx delete mode 100644 Makefile delete mode 100644 docker-compose-test.yml delete mode 100644 resources/test-jwt-nginx.conf create mode 100755 scripts.sh delete mode 100755 test.sh create mode 100644 test/Dockerfile-test-nginx rename Dockerfile-test-runner => test/Dockerfile-test-runner (100%) create mode 100644 test/docker-compose-test.yml rename {resources => test}/rsa_key_2048-pub.pem (100%) rename {resources => test}/rsa_key_2048.pem (100%) create mode 100644 test/test.conf create mode 100755 test/test.sh diff --git a/.bin/git/hooks-wrapper b/.bin/git/hooks-wrapper new file mode 100755 index 0000000..33cea38 --- /dev/null +++ b/.bin/git/hooks-wrapper @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Runs all executable pre-commit-* hooks and exits after, +# if any of them was not successful. +# +# Based on +# https://github.com/ELLIOTTCABLE/Paws.js/blob/Master/Scripts/git-hooks/chain-hooks.sh +# http://osdir.com/ml/git/2009-01/msg00308.html +# +# assumes your scripts are located at /bin/git/hooks + +exitcodes=() +hookname=`basename $0` +# our special hooks folder +CUSTOM_HOOKS_DIR=$(git rev-parse --show-toplevel)/bin/git/hooks +# find gits native hooks folder +NATIVE_HOOKS_DIR=$(git rev-parse --show-toplevel)/.git/hooks + +# Run each hook, passing through STDIN and storing the exit code. +# We don't want to bail at the first failure, as the user might +# then bypass the hooks without knowing about additional issues. + +for hook in ${CUSTOM_HOOKS_DIR}/$(basename $0)-*; do + test -x "$hook" || continue + + echo "Running custom hook '$hookname' ..." + out=`$hook "$@"` + exitcodes+=($?) + echo "$out" +done + +# check if there was a local hook that was moved previously +if [ -f "${NATIVE_HOOKS_DIR}/$hookname.local" ]; then + echo "Running native hook '$hookname' ..." + out=`${NATIVE_HOOKS_DIR}/$hookname.local "$@"` + exitcodes+=($?) + echo "$out" +fi + +# If any exit code isn't 0, bail. +for i in "${exitcodes[@]}"; do + [ "$i" == 0 ] || exit $i +done \ No newline at end of file diff --git a/.bin/git/hooks/pre-push-build-and-test b/.bin/git/hooks/pre-push-build-and-test new file mode 100755 index 0000000..9cf4682 --- /dev/null +++ b/.bin/git/hooks/pre-push-build-and-test @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +REPO_ROOT_DIR=$(git rev-parse --show-toplevel) +CHANGE_COUNT=$(cd ${REPO_ROOT_DIR}; git diff --name-only origin/HEAD..HEAD -- resources/ src/ test/ Dockerfile scripts.sh |wc -l) + +if [[ "0" -ne "${CHANGE_COUNT}" ]]; then + (cd ${REPO_ROOT_DIR}; ./scripts.sh rebuild_nginx rebuild_test_runner test) +else + HOOK_NAME=$(basename $0) + + echo "Skipping hook '${HOOK_NAME}' -- no changes detected which would require tests to be run." +fi diff --git a/.bin/git/init-hooks b/.bin/git/init-hooks new file mode 100755 index 0000000..1513105 --- /dev/null +++ b/.bin/git/init-hooks @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# based on http://stackoverflow.com/a/3464399/1383268 +# assumes that the hooks-wrapper script is located at /bin/git/hooks-wrapper + +HOOK_NAMES="applypatch-msg pre-applypatch post-applypatch pre-commit prepare-commit-msg commit-msg post-commit pre-rebase post-checkout post-merge pre-receive update post-receive post-update pre-auto-gc pre-push" +# find git's native hooks folder +REPO_ROOT_DIR=$(git rev-parse --show-toplevel) +HOOKS_DIR=$(git rev-parse --show-toplevel)/.git/hooks + +for hook in ${HOOK_NAMES}; do + # If the hook already exists, is a file, and is not a symlink + if [ ! -h ${HOOKS_DIR}/${hook} ] && [ -f ${HOOKS_DIR}/${hook} ]; then + mv ${HOOKS_DIR}/${hook} ${HOOKS_DIR}/${hook}.local + fi + # create the symlink, overwriting the file if it exists + # probably the only way this would happen is if you're using an old version of git + # -- back when the sample hooks were not executable, instead of being named ____.sample + ln -s -f ${REPO_ROOT_DIR}/bin/git/hooks-wrapper ${HOOKS_DIR}/${hook} +done \ No newline at end of file diff --git a/.bin/init b/.bin/init new file mode 100755 index 0000000..9c59222 --- /dev/null +++ b/.bin/init @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +source $(dirname $0)/git/init-hooks \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ad36f02..b0712ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -ARG NGINX_VERSION=1.22.0 +ARG NGINX_VERSION FROM debian:bullseye-slim as BASE_IMAGE LABEL stage=builder -RUN apt-get update \ +RUN apt-get update \ && apt-get install -y curl build-essential @@ -11,30 +11,31 @@ FROM BASE_IMAGE as BUILD_IMAGE LABEL stage=builder ENV LD_LIBRARY_PATH=/usr/local/lib ARG NGINX_VERSION -ADD . /root/dl/ngx-http-auth-jwt-module -RUN set -x \ +RUN set -x \ && apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev \ - && mkdir -p /root/dl -WORKDIR /root/dl -RUN set -x \ - && curl -O http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz \ - && tar -xzf nginx-$NGINX_VERSION.tar.gz \ - && rm nginx-$NGINX_VERSION.tar.gz \ - && ln -sf nginx-$NGINX_VERSION nginx \ - && cd /root/dl/nginx \ - && ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module \ + && mkdir -p /root/build/ngx-http-auth-jwt-module +WORKDIR /root/build/ngx-http-auth-jwt-module +ADD config ./ +ADD src/*.h src/*.c ./src/ +WORKDIR /root/build +RUN set -x \ + && mkdir nginx \ + && curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \ + && tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx \ + && rm nginx-${NGINX_VERSION}.tar.gz +WORKDIR /root/build/nginx +RUN ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module \ && make modules FROM nginx:${NGINX_VERSION} LABEL stage=builder -RUN apt-get update \ - && apt-get -y install libjansson4 libjwt0 \ +RUN apt-get update \ + && apt-get -y install libjansson4 libjwt0 \ && cd /etc/nginx \ - && cp nginx.conf nginx.conf.orig \ && sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf LABEL stage= LABEL maintainer="TeslaGov" email="developers@teslagov.com" -COPY --from=BUILD_IMAGE /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ +COPY --from=BUILD_IMAGE /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ diff --git a/Dockerfile-test-nginx b/Dockerfile-test-nginx deleted file mode 100644 index 10a1eae..0000000 --- a/Dockerfile-test-nginx +++ /dev/null @@ -1,10 +0,0 @@ -ARG BASE_IMAGE=teslagov/jwt-nginx:latest - -FROM ${BASE_IMAGE} as NGINX -COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf -COPY resources/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure \ - && cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 \ - && cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256-file \ - && cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header \ - && cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect diff --git a/Makefile b/Makefile deleted file mode 100644 index 17c7aa2..0000000 --- a/Makefile +++ /dev/null @@ -1,73 +0,0 @@ -SHELL += -eu - -BLUE := \033[0;34m -GREEN := \033[0;32m -RED := \033[0;31m -NC := \033[0m - -DOCKER_ORG_NAME ?= teslagov -DOCKER_IMAGE_NAME ?= jwt-nginx -COMPOSE_PROJECT_NAME ?= jwt-nginx-test -NGINX_VERSION ?= 1.22.0 - -.PHONY: all -all: - @$(MAKE) build-nginx - @$(MAKE) start-nginx - @$(MAKE) test - -.PHONY: build-nginx -build-nginx: - @echo "${BLUE} Building...${NC}" - @docker image pull debian:bullseye-slim - @docker image pull nginx:${NGINX_VERSION} - @docker image build -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:latest -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} . ; \ - SUCCESS=$$? ; \ - docker rmi $$(docker images --filter=label=stage=builder --quiet); \ - if [ "$$SUCCESS" -ne 0 ] ; \ - then echo "${RED} Build failed ${NC}"; \ - else echo "${GREEN}✓ Successfully built NGINX module ${NC}"; fi - -.PHONY: rebuild-nginx -rebuild-nginx: - @echo "${BLUE} Rebuilding...${NC}" - @docker image pull debian:bullseye-slim - @docker image pull nginx:${NGINX_VERSION} - @docker image build -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:latest -t ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} --no-cache .; \ - SUCCESS=$$? ; \ - docker rmi $$(docker images --filter=label=stage=builder --quiet); \ - if [ "$$SUCCESS" -ne 0 ] ; \ - then echo "${RED} Build failed ${NC}"; \ - else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}"; fi - -.PHONY: stop-nginx -stop-nginx: - docker stop "${DOCKER_IMAGE_NAME}" - -.PHONY: start-nginx -start-nginx: - docker run --rm --name "${DOCKER_IMAGE_NAME}" -d -p 8000:80 ${DOCKER_ORG_NAME}/${DOCKER_IMAGE_NAME} - -.PHONY: cp-bin -cp-bin: start-nginx - rm -rf bin - mkdir -p bin - docker exec jwt-nginx sh -c "tar -chf - \ - /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ - /usr/lib/x86_64-linux-gnu/libjansson.so.* \ - /usr/lib/x86_64-linux-gnu/libjwt.*" 2>/dev/null | tar -xf - -C bin &>/dev/null - -.PHONY: build-test-runner -build-test-runner: - IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml build - -.PHONY: rebuild-test-runner -rebuild-test-runner: - IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml build --no-cache - -.PHONY: test -test: - IMAGE_VERSION=${NGINX_VERSION} docker compose -f ./docker-compose-test.yml up --no-start - docker start ${COMPOSE_PROJECT_NAME}-nginx-1 - docker start -a ${COMPOSE_PROJECT_NAME}-runner-1 - docker compose -f ./docker-compose-test.yml down diff --git a/README.md b/README.md index ab4f8cd..84913ee 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,96 @@ # Intro + This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. ## Building and testing + To build the Docker image, start NGINX, and run our Bash test against it, run ```bash -make +./scripts.sh all ``` -When you make a change to the module, run `make rebuild-nginx`. +When you make a change to the module or the NGINX test config, run `./scripts.sh rebuild_nginx` to rebuild the NGINX Docker image. -When you make a change to `test.sh`, run `make rebuild-test-runner`. +When you make a change to `test.sh`, run `./scripts.sh rebuild_test_runner test` to rebuild the test runner image and run the tests. -| Command | Description | -| -------------------------- |:-----------------------------------------------------------------:| -| `make build-nginx` | Builds the NGINX image | -| `make rebuild-nginx` | Re-builds the NGINX image | -| `make build-test-runner` | Builds the images used by the test stack (uses Docker compose) | -| `make rebuild-test-runner` | Re-builds the images used by the test stack | -| `make start-nginx` | Starts the NGINX container | -| `make stop-nginx` | Stops the NGINX container | -| `make test` | Runs `test.sh` against the NGINX container (uses Docker compose) | +The `./scripts.sh` file contains multiple commands to make things easy: -The image produced with `make build-nginx` only differs from the official Nginx image in two ways: the module itself and the nginx.conf configuration entry that loads it. +| Command | Description | +| --------------------- | ----------------------------------------------------------------- | +| `build_nginx` | Builds the NGINX image. | +| `rebuild_nginx` | Re-builds the NGINX image. | +| `start_nginx` | Starts the NGINX container. | +| `stop_nginx` | Stops the NGINX container. | +| `cp_bin` | Copies the compiled binaries out of the NGINX container. | +| `build_test_runner` | Builds the images used by the test stack (uses Docker compose). | +| `rebuild_test_runner` | Re-builds the images used by the test stack. | +| `test` | Runs `test.sh` against the NGINX container (uses Docker compose). | -The tests use a customized Nginx image, distinct from the main image, as well as a test runner image. By running `make test`, the two test containers will be created up with Docker compose, started, and the tests run. At the end, both containers will be automatically stopped and destroyed. +You can run multiple commands in sequence by separating them with a space, e.g.: -This project is 100% Docker-ized. +```shell +./scripts.sh rebuild_nginx rebuild_test_runner test +``` -## Dependencies -This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt) +The image produced with `./scripts.sh build_nginx` only differs from the official NGINX image in two ways: + - the JWT module itself, and + - the `nginx.conf` file is overwritten with our own. -Transitively, that library depends on a JSON Parser called -[Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. +The tests use a customized NGINX image, distinct from the main image, as well as a test runner image. By running `./scripts.sh test`, the two test containers will be stood up via Docker compose, then they'll be started, and the tests will run. At the end of the test run, both containers will be automatically stopped and destroyed. See below to learn how to trace test failures across runs. -## NGINX Directives -This module requires several new `nginx.conf` directives, -which can be specified in on the `main` `server` or `location` level. +### Tracing test failures + +After making changes and finding that some tests fail, it can be difficult to understand why. By default, logs are written to Docker's internal log mechanism, but they won't be persisted after the test run completes and the containers are removed. + +In order to persist logs, you can configure the log driver to use. You can do this by setting the environment variable `LOG_DRIVER` before running the tests. On Linux/Unix systems, you can use the driver `journald`, as follows: + +```shell +# need to rebuild the test runner with the proper log driver +LOG_DRIVER=journald ./scripts.sh rebuild_test_runner +# run the tests +./scripts.sh test + +# check the logs +journalctl -eu docker CONTAINER_NAME=jwt-nginx-test ``` -auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; # see docs for format based on algorithm -auth_jwt_loginurl "https://yourdomain.com/loginpage"; -auth_jwt_enabled on; -auth_jwt_algorithm HS256; # or RS256 -auth_jwt_extract_sub on; # or off -auth_jwt_validate_email on; # or off -auth_jwt_use_keyfile off; # or on -auth_jwt_keyfile_path "/app/pub_key"; + +Now you'll be able to see logs from previous test runs. The best way to make use of this is to open two terminals, one where you run the tests, and one where you follow the logs: + +```shell +# terminal 1 +./scripts.sh test + +# terminal 2 +journalctl -fu docker CONTAINER_NAME=jwt-nginx-test ``` -The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms, Section 5.3.2 The HMAC Key. +## Dependencies + +This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt). Transitively, that library depends on a JSON Parser called [Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. + +## NGINX Directives +This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. + +| Directive | Description | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `auth_jwt_key` | The key to use to decode/verify the JWT -- see below. | +| `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | +| `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | +| `auth_jwt_enabled` | Set to "on" to enable JWT checking. | +| `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | +| `auth_jwt_extract_sub` | Set to "on" to extract the `sub` claim (e.g. user id) from the JWT and into the `x-userid` header on the response. | +| `auth_jwt_validate_email` | Set to "on" to extract the `emailAddress` claim from the JWT and into the `x-email` header on the response. | +| `auth_jwt_use_keyfile` | Set to "on" to read the key from a file rather than from the `auth_jwt_key` directive. | +| `auth_jwt_keyfile_path` | Set to the path from which the key should be read when `auth_jwt_use_keyfile` is enabled. | + + +The default algorithm is `HS256`, for symmetric key validation. When using one of the `HS*` algorithms, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see [NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final), Section 5.3.2 The HMAC Key. + +The configuration also supports RSA public key validation via (e.g.) `auth_jwt_algorithm RS256`. When using the `RS*` alhorithms, the `auth_jwt_key` field must be set to your public key **OR** `auth_jwt_use_keyfile` should be set to `on` and `auth_jwt_keyfile_path` should point to the public key on disk. NGINX won't start if `auth_jwt_use_keyfile` is set to `on` and a key file is not provided. -The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key **OR** `auth_jwt_use_keyfile` should be set to `on` with the `auth_jwt_keyfile_path` set to the public key path (nginx won't start if the `auth_jwt_use_keyfile` is set to `on` without a keyfile). -That is the public key, rather than a PEM certificate. I.e.: +When using an `RS*` algorithm with an inline key, be sure to set `auth_jwt_key` to the _public key_, rather than a PEM certificate. E.g.: ``` auth_jwt_key "-----BEGIN PUBLIC KEY----- @@ -66,40 +104,44 @@ oQIDAQAB -----END PUBLIC KEY-----"; ``` -**OR** +When using an `RS*` algorithm with a public key file, do as follows: ``` auth_jwt_use_keyfile on; -auth_jwt_keyfile_path "/etc/nginx/pub_key.pem"; +auth_jwt_keyfile_path "/path/to/pub_key.pem"; ``` -A typical use would be to specify the key and loginurl on the main level -and then only turn on the locations that you want to secure (not the login page). -Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. +A typical use would be to specify the key and login URL at the `http` level, and then only turn JWT authentication on for the locations which you want to secure. Unauthorized requests result in a 302 "Moved Temporarily" response with the `Location` header set to the URL specified in the `auth_jwt_loginurl` directive, and a querystring parameter `return_url` whose value is the current / attempted URL. + +If you prefer to return `401 Unauthorized` rather than redirect, you may turn `auth_jwt_redirect` off: ``` -auth_jwt_redirect off; +auth_jwt_redirect off; ``` -If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. + +By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT: ``` -auth_jwt_validation_type AUTHORIZATION; auth_jwt_validation_type COOKIE=jwt; ``` -By default the authorization header is used to provide a JWT for validation. -However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. -``` -auth_jwt_extract_sub -``` By default, the module will attempt to extract the `sub` claim (e.g. the user's id) from the JWT. If successful, the value will be set in the `x-userid` HTTP header. An error will be logged if this option is enabled and the JWT does not -contain the `sub` claim. +contain the `sub` claim. You may disable this option as follows: ``` -auth_jwt_validate_email off; +auth_jwt_extract_sub off ``` + By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the -session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different +session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different user identifier property such as `sub`, set `auth_jwt_validate_email` to the value `off`. _Note that this flag may be -renamed to `auth_jwt_extract_email` in a future release._ +renamed to `auth_jwt_extract_email` in a future release._ You may disable this option as follows: + +``` +auth_jwt_validate_email off; +``` + +## Contributing + +If you'd like to contribute to this repository, please first initiate the Git hooks by running `./.bin/init` (note the `.` before `bin`) -- this will ensure that tests are run before you push your changes. diff --git a/config b/config index 317aeea..34154fb 100644 --- a/config +++ b/config @@ -1,8 +1,7 @@ -ngx_addon_name=ngx_http_auth_jwt_module - ngx_module_type=HTTP -ngx_module_name=ngx_http_auth_jwt_module -ngx_module_srcs="$ngx_addon_dir/src/ngx_http_auth_jwt_binary_converters.c $ngx_addon_dir/src/ngx_http_auth_jwt_header_processing.c $ngx_addon_dir/src/ngx_http_auth_jwt_string.c $ngx_addon_dir/src/ngx_http_auth_jwt_module.c" +ngx_addon_name=ngx_http_auth_jwt_module +ngx_module_name=$ngx_addon_name +ngx_module_srcs="${ngx_addon_dir}/src/ngx_http_auth_jwt_binary_converters.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_header_processing.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_string.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_module.c" ngx_module_libs="-ljansson -ljwt" . auto/module diff --git a/docker-compose-test.yml b/docker-compose-test.yml deleted file mode 100644 index 021d191..0000000 --- a/docker-compose-test.yml +++ /dev/null @@ -1,20 +0,0 @@ -version: '3.3' - -services: - - nginx: - build: - context: . - dockerfile: Dockerfile-test-nginx - args: - BASE_IMAGE: ${IMAGE_NAME:-teslagov/jwt-nginx}:${IMAGE_VERSION:-latest} - - runner: - build: - context: . - dockerfile: Dockerfile-test-runner - environment: - BASE_IMAGE: ${IMAGE_NAME:-teslagov/jwt-nginx}:${IMAGE_VERSION:-latest} - - depends_on: - - nginx \ No newline at end of file diff --git a/resources/nginx.conf b/resources/nginx.conf index 3981730..9b8feab 100644 --- a/resources/nginx.conf +++ b/resources/nginx.conf @@ -1,4 +1,3 @@ - user nginx; worker_processes auto; @@ -6,28 +5,21 @@ error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; - events { worker_connections 1024; } - http { - include /etc/nginx/mime.types; - default_type application/octet-stream; + include /etc/nginx/mime.types; + default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; - access_log /var/log/nginx/access.log main; - - sendfile on; - #tcp_nopush on; - - keepalive_timeout 65; - - #gzip on; + sendfile on; + keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; -} \ No newline at end of file +} diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf deleted file mode 100644 index 53f512b..0000000 --- a/resources/test-jwt-nginx.conf +++ /dev/null @@ -1,63 +0,0 @@ -server { - auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; - auth_jwt_loginurl "https://teslagov.com"; - auth_jwt_enabled off; - auth_jwt_redirect on; - - listen 8000; - server_name localhost; - - location ~ ^/secure-no-redirect/ { - auth_jwt_enabled on; - auth_jwt_redirect off; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure/ { - auth_jwt_enabled on; - auth_jwt_validation_type COOKIE=jwt; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure-auth-header/ { - auth_jwt_enabled on; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure-rs256/ { - auth_jwt_enabled on; - auth_jwt_validation_type COOKIE=jwt; - auth_jwt_algorithm RS256; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh -uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ -iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM -ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g -6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe -K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t -BwIDAQAB ------END PUBLIC KEY-----"; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure-rs256-file/ { - auth_jwt_enabled on; - auth_jwt_validation_type AUTHORIZATION; - auth_jwt_algorithm RS256; - auth_jwt_redirect off; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; - root /usr/share/nginx; - index index.html index.htm; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } -} - diff --git a/scripts.sh b/scripts.sh new file mode 100755 index 0000000..e41620d --- /dev/null +++ b/scripts.sh @@ -0,0 +1,89 @@ +#!/bin/bash -eu + +BLUE='\033[0;34m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +export ORG_NAME=${ORG_NAME:-teslagov} +export IMAGE_NAME=${IMAGE_NAME:-jwt-nginx} +export FULL_IMAGE_NAME=${ORG_NAME}/${IMAGE_NAME} +export CONTAINER_NAME_PREFIX=${CONTAINER_NAME_PREFIX:-jwt-nginx-test} +export NGINX_VERSION=${NGINX_VERSION:-1.22.0} + +all() { + build_nginx + start_nginx + test +} + +fetch_headers() { + printf "${BLUE} Fetching NGINX headers...${NC}" + local files='src/core/ngx_core.h src/http/ngx_http.h' + + for f in ${files}; do + curl "https://raw.githubusercontent.com/nginx/nginx/release-${NGINX_VERSION}/${f}" -o src/lib/$(basename ${f}) + done +} + +build_nginx() { + local dockerArgs=${1:-} + + printf "${BLUE} Building...${NC}" + docker image pull debian:bullseye-slim + docker image pull nginx:${NGINX_VERSION} + docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} ${dockerArgs} . + + if [ "$?" -ne 0 ]; then + printf "${RED} Build failed ${NC}" + else + printf "${GREEN}✓ Successfully built NGINX module ${NC}" + fi + + docker rmi -f $(docker images --filter=label=stage=builder --quiet) +} + +rebuild_nginx() { + build_nginx --no-cache +} + +start_nginx() { + docker run --rm --name "${IMAGE_NAME}" -d -p 8000:80 ${FULL_IMAGE_NAME} +} + +stop_nginx() { + docker stop "${IMAGE_NAME}" +} + +cp_bin() { + printf "${BLUE} Copying binaries...${NC}" + rm -rf bin + mkdir bin + docker exec "${IMAGE_NAME}" sh -c "tar -chf - \ + /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ + /usr/lib/x86_64-linux-gnu/libjansson.so.* \ + /usr/lib/x86_64-linux-gnu/libjwt.*" 2>/dev/null | tar -xf - -C bin &>/dev/null +} + +build_test_runner() { + local dockerArgs=${1:-} + + printf "${BLUE} Building test runner...${NC}" + docker compose -f ./test/docker-compose-test.yml build ${dockerArgs} +} + +rebuild_test_runner() { + build_test_runner --no-cache +} + +test() { + printf "${BLUE} Running tests...${NC}" + docker compose -f ./test/docker-compose-test.yml up --no-start + docker start ${CONTAINER_NAME_PREFIX} + docker start -a ${CONTAINER_NAME_PREFIX}-runner + docker compose -f ./test/docker-compose-test.yml down +} + +for fn in $@; do + "$fn" +done \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 4209e3c..cb3c055 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -182,7 +182,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (jwtPtr == NULL) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a JWT"); goto redirect; } @@ -190,7 +190,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; - if (auth_jwt_algorithm.len == 0 || (auth_jwt_algorithm.len == sizeof("HS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "HS256", sizeof("HS256") - 1)==0)) + if (auth_jwt_algorithm.len == 0 || (auth_jwt_algorithm.len == 5 && ngx_strncmp(auth_jwt_algorithm.data, "HS", 2) == 0)) { keylen = jwtcf->auth_jwt_key.len / 2; keyBinary = ngx_palloc(r->pool, keylen); @@ -200,7 +200,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) goto redirect; } } - else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) + else if ( auth_jwt_algorithm.len == 5 && ngx_strncmp(auth_jwt_algorithm.data, "RS", 2) == 0 ) { // in this case, 'Binary' is a misnomer, as it is the public key string itself if (jwtcf->auth_jwt_use_keyfile == 1) @@ -218,7 +218,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) } else { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm"); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm %s", auth_jwt_algorithm); goto redirect; } @@ -227,16 +227,16 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (jwtParseReturnCode != 0) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt, error code %d", jwtParseReturnCode); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT, error code %d", jwtParseReturnCode); goto redirect; } // validate the algorithm alg = jwt_get_alg(jwt); - if (alg != JWT_ALG_HS256 && alg != JWT_ALG_RS256) + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in jwt %d", alg); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in JWT (%d)", alg); goto redirect; } @@ -246,7 +246,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (exp < now) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt has expired"); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); goto redirect; } @@ -257,7 +257,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (sub == NULL) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain a subject"); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); } else { @@ -273,7 +273,7 @@ static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) if (email == NULL) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain an email address"); } else { diff --git a/test.sh b/test.sh deleted file mode 100755 index 45d21ee..0000000 --- a/test.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -RED='\033[01;31m' -GREEN='\033[01;32m' -NONE='\033[00m' - -test_jwt () { - local name=$1 - local path=$2 - local expect=$3 - local extra=$4 - - cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://nginx:8000$path -H 'cache-control: no-cache' $extra" - test=$( eval ${cmd} ) - - if [ "$test" -eq "$expect" ]; then - echo -e "${GREEN}${name}: passed (${test})${NONE}"; - else - echo -e "${RED}${name}: failed (${test})${NONE}"; - fi -} - -main() { - local VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 - local MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q - local MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM - local VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ - local INVALID_RSA256_JWT=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 - - test_jwt "Insecure test" "/" "200" - - test_jwt "Secure test without jwt cookie" "/secure/" "302" - - test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"jwt=${VALIDJWT}\"" - - test_jwt "Secure test with jwt auth header" "/secure-auth-header/" "200" "--header \"Authorization: Bearer ${VALIDJWT}\"" - - test_jwt "Secure test without jwt auth header" "/secure-auth-header/" "302" - - test_jwt "Secure test with jwt auth header missing Bearer" "/secure-no-redirect/" "401" "--header \"Authorization: X\"" - - test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" - - test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"jwt=${MISSING_SUB_JWT}\"" - - test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"jwt=${MISSING_EMAIL_JWT}\"" - - test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"jwt=${VALID_RS256_JWT}\"" - - test_jwt "Secure test rsa256 from file with valid jwt" "/secure-rs256-file/" "200" "--header \"Authorization: Bearer ${VALID_RS256_JWT}\"" - - test_jwt "Secure test rsa256 from file with invalid jwt" "/secure-rs256-file/" "401" "--header \"Authorization: Bearer ${INVALID_RSA256_JWT}\"" -} - -main "$@" diff --git a/test/Dockerfile-test-nginx b/test/Dockerfile-test-nginx new file mode 100644 index 0000000..c1e4550 --- /dev/null +++ b/test/Dockerfile-test-nginx @@ -0,0 +1,5 @@ +ARG BASE_IMAGE + +FROM ${BASE_IMAGE} as NGINX +COPY test.conf /etc/nginx/conf.d/test.conf +COPY rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf \ No newline at end of file diff --git a/Dockerfile-test-runner b/test/Dockerfile-test-runner similarity index 100% rename from Dockerfile-test-runner rename to test/Dockerfile-test-runner diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml new file mode 100644 index 0000000..2607e42 --- /dev/null +++ b/test/docker-compose-test.yml @@ -0,0 +1,24 @@ +version: '3.3' + +services: + + nginx: + container_name: ${CONTAINER_NAME_PREFIX} + build: + context: . + dockerfile: Dockerfile-test-nginx + args: + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} + logging: + driver: ${LOG_DRIVER:-journald} + + runner: + container_name: ${CONTAINER_NAME_PREFIX}-runner + build: + context: . + dockerfile: Dockerfile-test-runner + environment: + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} + + depends_on: + - nginx \ No newline at end of file diff --git a/resources/rsa_key_2048-pub.pem b/test/rsa_key_2048-pub.pem similarity index 100% rename from resources/rsa_key_2048-pub.pem rename to test/rsa_key_2048-pub.pem diff --git a/resources/rsa_key_2048.pem b/test/rsa_key_2048.pem similarity index 100% rename from resources/rsa_key_2048.pem rename to test/rsa_key_2048.pem diff --git a/test/test.conf b/test/test.conf new file mode 100644 index 0000000..52c2829 --- /dev/null +++ b/test/test.conf @@ -0,0 +1,134 @@ +server { + listen 8000; + server_name localhost; + + auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; + auth_jwt_loginurl "https://example.com/login"; + auth_jwt_enabled off; + + location / { + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default/no-redirect { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type COOKIE=jwt; + auth_jwt_algorithm HS256; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type COOKIE=jwt; + auth_jwt_algorithm HS384; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type COOKIE=jwt; + auth_jwt_algorithm HS512; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type AUTHORIZATION; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default/no-redirect { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh +uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ +iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM +ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g +6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe +K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t +BwIDAQAB +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs256/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_algorithm RS256; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs384/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_algorithm RS384; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs512/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_algorithm RS512; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } +} + diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..b6ec12c --- /dev/null +++ b/test/test.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +RED='\033[01;31m' +GREEN='\033[01;32m' +NONE='\033[00m' + +run_test () { + local name=$1 + local path=$2 + local expect=$3 + local extra=$4 + + cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://nginx:8000${path} -H 'cache-control: no-cache' $extra" + result=$(eval ${cmd}) + + if [ "${result}" -eq "${expect}" ]; then + echo -e "${GREEN}${name}: passed (Received: ${result}; Path: ${path})${NONE}"; + return 0 + else + echo -e "${RED}${name}: failed (Expected: ${expect}; Received: ${result}; Path: ${path})${NONE}"; + return 1 + fi +} + +main() { + local JWT_HS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.r8tG8IZheiQ-i6HqUYyJj9V6dipgcQ4ZIdxau6QCZDo + local JWT_HS256_MISSING_SUB=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q + local JWT_HS256_MISSING_EMAIL=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM + local JWT_HS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.SS57j7PEybjbsp3g5W-IhhJHBmG5K-97qvgBKL16xj9ey-uMeEenWjGbB2vVp0kq + local JWT_HS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.xtSU6EWN2LILVsYzJFJpKnRkqjn_3qjz-J2ttNKnhZ60_5YjFeC8io4k8k1u77zlohSWvWMdugD9ZaB3vjJo-w + local JWT_RS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ + local JWT_RS256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 + local JWT_RS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.H35bTcZRhepWIoa8pKCbUMRuAOkVX9K5hJjc6tPmQwWmTw8lrktsvmMzJg_rgqnJLnAkciSIQw5EDj7fngS5zX2ThyRxrkPuE2Uiyw2Ect-mo9Kg1lrWgnyZCuCgq-Up9HQRAv0160mePlm8Gs4TOY6CPr38zwTcDZsy_Keq93igDQV8WuuWAGICaGd5ZyUOPjjzGShRjTU8Szz7fnpZpTtYRCYVo0pc5yfRWYm0fdn-4AseyGvd8JJ2xfnAEe4kZOkz7X1MLKtL0slKg3m2PH1lD7HwxIawXRTPWxArhJ9dcTNiDUrqtde2juGwOuMD_zTsb2Jj0_rmRb0Q6aljNw + local JWT_RS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.iUupyKypfXJ5aZWfItSW-mOmx9a4C4X7Yr5p5Fk8W75ZhkOq0EeNfstTxx870brhkdPovBhO2LYI44_HoH9XicQNL6JnFprE0r61eJFngbuzlhRQiWpq0xYrazJWc9zB7_GgL2ZCwtw-Ts3G23Q0632wVm6-d7MKvG7RS8aEjN-MuVGdtLglH3forpItmFxw-if40EQsBL7hncN_XNcQTO4KPHkqmlpac_oKXRrLFDIIt2tB6OOpvY4QcpERoxexp4pi2f-JoINnWX_dU5JnIs3ypVJLQPfoJvxg8fsg3zYrOvMYnfsqOCYoHtZGK0O7jyfFmcGo5v2hLT-CpoF3Zw + local num_tests=0 + local num_failed=0 + + run_test 'when auth disabled, should return 200' \ + '/' \ + '200' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + '/secure/auth-header/default' \ + '302' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm with no redirect and Authroization header missing Bearer, should return 401' \ + '/secure/auth-header/default/no-redirect' \ + '401' \ + '--header "Authorization: X"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ + '/secure/cookie/default' \ + '302' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm with no redirect and no JWT cookie, should return 401' \ + '/secure/cookie/default/no-redirect' \ + '401' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm and valid JWT cookie, returns 200' \ + '/secure/cookie/default' \ + '200' \ + '--cookie "jwt=${JWT_HS256_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm and valid JWT cookie with no sub, returns 200' \ + '/secure/cookie/default' \ + '200' \ + ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with default algorithm and valid JWT cookie with no email, returns 200' \ + '/secure/cookie/default' \ + '200' \ + ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ + '/secure/cookie/hs256/' \ + '200' \ + '--cookie "jwt=${JWT_HS256_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with HS384 algorithm and valid JWT cookie, returns 200' \ + '/secure/cookie/hs384' \ + '200' \ + '--cookie "jwt=${JWT_HS384_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with HS512 algorithm and valid JWT cookie, returns 200' \ + '/secure/cookie/hs512' \ + '200' \ + '--cookie "jwt=${JWT_HS512_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with RS256 algorithm and valid JWT cookie, returns 200' \ + '/secure/cookie/rs256' \ + '200' \ + ' --cookie "jwt=${JWT_RS256_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ + '/secure/auth-header/rs256/file' \ + '200' \ + '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with RS256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + '/secure/auth-header/rs256/file' \ + '302' \ + '--header "Authorization: Bearer ${JWT_RS256_INVALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with RS384 algorithm via file and valid JWT in Authorization header, returns 200' \ + '/secure/auth-header/rs384/file' \ + '200' \ + '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + run_test 'when auth enabled with RS512 algorithm via file and valid JWT in Authorization header, returns 200' \ + '/secure/auth-header/rs512/file' \ + '200' \ + '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); + + if [[ "${num_failed}" = '0' ]]; then + printf "\nRan ${num_tests} tests successfully.\n" + return 0 + else + printf "\nRan ${num_tests} tests: ${GREEN}$((${num_tests} - ${num_failed})) passed${NONE}; ${RED}${num_failed} failed${NONE}\n" + return 1 + fi +} + +main '$@' From 69e6e53f814a83b95b1cd06bf49ffcc3b079574f Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Wed, 9 Nov 2022 17:36:21 +0200 Subject: [PATCH 071/130] Fix link with jansson (#63) jansson/lib/libjansson.a(value.o): In function `json_real': value.c:(.text+0x3134): undefined reference to `__isnan' value.c:(.text+0x314c): undefined reference to `__isinf' jansson/lib/libjansson.a(value.o): In function `json_real_set': value.c:(.text+0x3294): undefined reference to `__isnan' value.c:(.text+0x32ac): undefined reference to `__isinf' collect2: error: ld returned 1 exit status Co-authored-by: Josh McCullough --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index 34154fb..13c2325 100644 --- a/config +++ b/config @@ -2,6 +2,6 @@ ngx_module_type=HTTP ngx_addon_name=ngx_http_auth_jwt_module ngx_module_name=$ngx_addon_name ngx_module_srcs="${ngx_addon_dir}/src/ngx_http_auth_jwt_binary_converters.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_header_processing.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_string.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_module.c" -ngx_module_libs="-ljansson -ljwt" +ngx_module_libs="-ljansson -ljwt -lm" . auto/module From 4cf353b3d1ab90c5786269270b6bc97f62fb070a Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 9 Nov 2022 15:17:19 -0500 Subject: [PATCH 072/130] update README with build-related info --- .gitignore | 1 + README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/.gitignore b/.gitignore index a676215..fe4f0be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea +.vscode bin diff --git a/README.md b/README.md index 84913ee..6237985 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,56 @@ auth_jwt_validate_email off; ## Contributing If you'd like to contribute to this repository, please first initiate the Git hooks by running `./.bin/init` (note the `.` before `bin`) -- this will ensure that tests are run before you push your changes. + +### Environment Set-up for Visual Studio Code + +1. Install the C/C++ extension from Microsoft. +2. Add a C/C++ config file at `.vscode/c_cpp_properties.json` with the following (or similar) content: + +```json +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "~/Projects/third-party/nginx/objs/**", + "~/Projects/third-party/nginx/src/**", + "~/Projects/third-party/libjwt/include/**", + "~/Projects/third-party/jansson/src/**" + ], + "defines": [], + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++14", + "intelliSenseMode": "linux-clang-x64" + } + ], + "version": 4 +} +``` + +Note the `includePath` additions above -- please update them as appropriate. Next we need to pull these sources. + +#### Building NGINX + +1. Download the NGINX release matching the version you're targeting. +2. Extract the NGINX archive to wherever you'd like. +3. Update the `includePath` entires shown above to match the location you chose. +4. Enter the directory where you extracted NGINX and run: `./configure --with-compat` + +### Cloning libjwt + +1. Clone this repository as follows (replace ``): `git clone git@github.com:benmcollins/libjwt.git +2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` +3. Update the `includePath` entires shown above to match the location you chose. + +### Cloning lobjansson + +1. Clone this repository as follows (replace ``): `git clone git@github.com:akheron/jansson.git +2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` +3. Update the `includePath` entires shown above to match the location you chose. + +### Verify Compliation + +Once you save your changes to `.vscode/c_cpp_properties.json`, you should see that warnings and errors in the Problems panel go away, at least temprorarily. Hopfeully they don't come back, but if they do, make sure your include paths are set correctly. From 829886f94a97fc3eb5f68f58db57b5fdb8dc42a4 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Thu, 10 Nov 2022 11:46:18 -0500 Subject: [PATCH 073/130] fix for #75 (#81) --- src/ngx_http_auth_jwt_module.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index cb3c055..508d781 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -422,31 +422,42 @@ ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) static ngx_int_t loadAuthKey(ngx_conf_t *cf, ngx_http_auth_jwt_loc_conf_t* conf) { FILE *keyFile = fopen((const char*)conf->auth_jwt_keyfile_path.data, "rb"); + unsigned long keySize; + unsigned long keySizeRead; // Check if file exists or is correctly opened if (keyFile == NULL) { - ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to open pub key file"); + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to open public key file"); return NGX_ERROR; } // Read file length fseek(keyFile, 0, SEEK_END); - long keySize = ftell(keyFile); + keySize = ftell(keyFile); fseek(keyFile, 0, SEEK_SET); if (keySize == 0) { - ngx_log_error(NGX_LOG_ERR, cf->log, 0, "invalid key file size, check the key file"); + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "invalid public key file size of 0"); return NGX_ERROR; } conf->_auth_jwt_keyfile.data = ngx_palloc(cf->pool, keySize); - fread(conf->_auth_jwt_keyfile.data, 1, keySize, keyFile); - conf->_auth_jwt_keyfile.len = (int)keySize; - + keySizeRead = fread(conf->_auth_jwt_keyfile.data, 1, keySize, keyFile); fclose(keyFile); - return NGX_OK; + + if (keySizeRead == keySize) + { + conf->_auth_jwt_keyfile.len = (int)keySize; + + return NGX_OK; + } + else { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "public key size %i does not match expected size of %i", keySizeRead, keySize); + + return NGX_ERROR; + } } static char * From a5656b19673bdbb6557dade0c7a445418333748a Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 15 Mar 2023 17:04:58 -0400 Subject: [PATCH 074/130] fix cp_bin function --- scripts.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts.sh b/scripts.sh index e41620d..01c307f 100755 --- a/scripts.sh +++ b/scripts.sh @@ -29,7 +29,7 @@ fetch_headers() { build_nginx() { local dockerArgs=${1:-} - printf "${BLUE} Building...${NC}" + printf "${BLUE} Building NGINX...${NC}" docker image pull debian:bullseye-slim docker image pull nginx:${NGINX_VERSION} docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} ${dockerArgs} . @@ -44,10 +44,12 @@ build_nginx() { } rebuild_nginx() { + printf "${BLUE} Rebuilding NGINX...${NC}" build_nginx --no-cache } start_nginx() { + printf "${BLUE} Starting NGINX...${NC}" docker run --rm --name "${IMAGE_NAME}" -d -p 8000:80 ${FULL_IMAGE_NAME} } @@ -56,13 +58,17 @@ stop_nginx() { } cp_bin() { + if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME})" != "true" ]; then + start_nginx + fi + printf "${BLUE} Copying binaries...${NC}" rm -rf bin mkdir bin - docker exec "${IMAGE_NAME}" sh -c "tar -chf - \ - /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ - /usr/lib/x86_64-linux-gnu/libjansson.so.* \ - /usr/lib/x86_64-linux-gnu/libjwt.*" 2>/dev/null | tar -xf - -C bin &>/dev/null + docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ + usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ + usr/lib/x86_64-linux-gnu/libjansson.so.* \ + usr/lib/x86_64-linux-gnu/libjwt.*" | tar -xf - -C bin &>/dev/null } build_test_runner() { From 8e5031b5b6a5767a2ad57ac9c852eb4b4554a489 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 19 Apr 2023 00:19:13 -0400 Subject: [PATCH 075/130] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6237985..546be26 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This module requires several new `nginx.conf` directives, which can be specified | Directive | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `auth_jwt_key` | The key to use to decode/verify the JWT -- see below. | +| `auth_jwt_key` | The key to use to decode/verify the JWT, *in binhex format* -- see below. | | `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | | `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | | `auth_jwt_enabled` | Set to "on" to enable JWT checking. | From 8014cdc01ac39406b75f26a61a0f6c77ea9244a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoshi=20J=C3=A4ger?= Date: Wed, 19 Apr 2023 13:55:18 +0200 Subject: [PATCH 076/130] Fix: Only call `docker rmi` if there are images to prune (#85) * fix(scripts.sh): only call `docker rmi` if there are images to prune * fix(docker-rmi): always return true after calling `docker rmi` --- scripts.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts.sh b/scripts.sh index 01c307f..89ec2ba 100755 --- a/scripts.sh +++ b/scripts.sh @@ -40,7 +40,7 @@ build_nginx() { printf "${GREEN}✓ Successfully built NGINX module ${NC}" fi - docker rmi -f $(docker images --filter=label=stage=builder --quiet) + docker rmi -f $(docker images --filter=label=stage=builder --quiet) || true } rebuild_nginx() { @@ -92,4 +92,4 @@ test() { for fn in $@; do "$fn" -done \ No newline at end of file +done From 850833443b3f616c5d250eb9b202266c8312f88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoshi=20J=C3=A4ger?= Date: Wed, 19 Apr 2023 14:04:03 +0200 Subject: [PATCH 077/130] fix(docker-buildkit): Make stage names lowercase (#84) --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b0712ea..3fc5d9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ ARG NGINX_VERSION -FROM debian:bullseye-slim as BASE_IMAGE +FROM debian:bullseye-slim as base_image LABEL stage=builder RUN apt-get update \ && apt-get install -y curl build-essential -FROM BASE_IMAGE as BUILD_IMAGE +FROM base_image as build_image LABEL stage=builder ENV LD_LIBRARY_PATH=/usr/local/lib ARG NGINX_VERSION @@ -38,4 +38,4 @@ RUN apt-get update \ LABEL stage= LABEL maintainer="TeslaGov" email="developers@teslagov.com" -COPY --from=BUILD_IMAGE /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ +COPY --from=build_image /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ From 583fffebe49871c2923c5e500046ba48f6ffe99c Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Thu, 20 Apr 2023 11:48:29 -0400 Subject: [PATCH 078/130] update to support extracting any claim to request/response headers + more (#87) * update to support extracting any cookie * fix tests * fix tests more * prefix log messages to find easier * try to fix array offset * fix test * extracting claims to request headers is working * add another test * refactor and cleanup * add claim extraction to response headers * rename functions and such for clarity * rename struct members for brevity _and_ clarity * rm debugging * update README * update README * update README * formatting * Update src/ngx_http_auth_jwt_header_processing.c I *think* it might be moot since the compiler will probably optimize it anyway, but might as well do it that way. Co-authored-by: Joan Marin * Update src/ngx_http_auth_jwt_module.c Co-authored-by: Joan Marin * Update src/ngx_http_auth_jwt_module.c Co-authored-by: Joan Marin --------- Co-authored-by: Joan Marin --- Dockerfile | 50 +- README.md | 242 +++-- scripts.sh | 79 +- src/ngx_http_auth_jwt_binary_converters.c | 66 +- src/ngx_http_auth_jwt_header_processing.c | 113 +-- src/ngx_http_auth_jwt_header_processing.h | 4 +- src/ngx_http_auth_jwt_module.c | 1105 +++++++++++---------- src/ngx_http_auth_jwt_string.c | 12 +- src/ngx_http_auth_jwt_string.h | 2 +- test/Dockerfile-test-nginx | 8 +- test/Dockerfile-test-runner | 10 +- test/docker-compose-test.yml | 1 + test/docker-entrypoint.d/10-nginx-test.sh | 1 + test/{ => etc/nginx/conf.d}/test.conf | 87 ++ test/{ => etc/nginx}/rsa_key_2048-pub.pem | 0 test/test.sh | 316 +++--- 16 files changed, 1227 insertions(+), 869 deletions(-) create mode 100755 test/docker-entrypoint.d/10-nginx-test.sh rename test/{ => etc/nginx/conf.d}/test.conf (59%) rename test/{ => etc/nginx}/rsa_key_2048-pub.pem (100%) diff --git a/Dockerfile b/Dockerfile index 3fc5d9e..328ea1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,41 @@ ARG NGINX_VERSION +ARG SOURCE_HASH -FROM debian:bullseye-slim as base_image -LABEL stage=builder -RUN apt-get update \ - && apt-get install -y curl build-essential +FROM debian:bullseye-slim as ngx_http_auth_jwt_builder_base +LABEL stage=ngx_http_auth_jwt_builder +RUN apt-get update &&\ + apt-get install -y curl build-essential -FROM base_image as build_image -LABEL stage=builder +FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module +LABEL stage=ngx_http_auth_jwt_builder ENV LD_LIBRARY_PATH=/usr/local/lib ARG NGINX_VERSION -RUN set -x \ - && apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev \ - && mkdir -p /root/build/ngx-http-auth-jwt-module +RUN set -x &&\ + apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev &&\ + mkdir -p /root/build/ngx-http-auth-jwt-module WORKDIR /root/build/ngx-http-auth-jwt-module +ARG SOURCE_HASH +RUN echo "Source Hash: ${SOURCE_HASH}" ADD config ./ ADD src/*.h src/*.c ./src/ WORKDIR /root/build -RUN set -x \ - && mkdir nginx \ - && curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \ - && tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx \ - && rm nginx-${NGINX_VERSION}.tar.gz +RUN set -x &&\ + mkdir nginx &&\ + curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz &&\ + tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx WORKDIR /root/build/nginx -RUN ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module \ - && make modules - - -FROM nginx:${NGINX_VERSION} -LABEL stage=builder -RUN apt-get update \ - && apt-get -y install libjansson4 libjwt0 \ - && cd /etc/nginx \ - && sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf +RUN ./configure --with-debug --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module &&\ + make modules +FROM nginx:${NGINX_VERSION} AS ngx_http_auth_jwt_builder_nginx LABEL stage= +RUN rm /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh /etc/nginx/conf.d/default.conf +RUN apt-get update &&\ + apt-get -y install libjansson4 libjwt0 &&\ + cd /etc/nginx &&\ + sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf LABEL maintainer="TeslaGov" email="developers@teslagov.com" -COPY --from=build_image /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ +COPY --from=ngx_http_auth_jwt_builder_module /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ diff --git a/README.md b/README.md index 546be26..3c7d0a7 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,41 @@ -# Intro +# Auth-JWT NGINX Module -This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. - -## Building and testing - -To build the Docker image, start NGINX, and run our Bash test against it, run - -```bash -./scripts.sh all -``` - -When you make a change to the module or the NGINX test config, run `./scripts.sh rebuild_nginx` to rebuild the NGINX Docker image. - -When you make a change to `test.sh`, run `./scripts.sh rebuild_test_runner test` to rebuild the test runner image and run the tests. - -The `./scripts.sh` file contains multiple commands to make things easy: - -| Command | Description | -| --------------------- | ----------------------------------------------------------------- | -| `build_nginx` | Builds the NGINX image. | -| `rebuild_nginx` | Re-builds the NGINX image. | -| `start_nginx` | Starts the NGINX container. | -| `stop_nginx` | Stops the NGINX container. | -| `cp_bin` | Copies the compiled binaries out of the NGINX container. | -| `build_test_runner` | Builds the images used by the test stack (uses Docker compose). | -| `rebuild_test_runner` | Re-builds the images used by the test stack. | -| `test` | Runs `test.sh` against the NGINX container (uses Docker compose). | - -You can run multiple commands in sequence by separating them with a space, e.g.: - -```shell -./scripts.sh rebuild_nginx rebuild_test_runner test -``` - -The image produced with `./scripts.sh build_nginx` only differs from the official NGINX image in two ways: - - the JWT module itself, and - - the `nginx.conf` file is overwritten with our own. - -The tests use a customized NGINX image, distinct from the main image, as well as a test runner image. By running `./scripts.sh test`, the two test containers will be stood up via Docker compose, then they'll be started, and the tests will run. At the end of the test run, both containers will be automatically stopped and destroyed. See below to learn how to trace test failures across runs. - -### Tracing test failures - -After making changes and finding that some tests fail, it can be difficult to understand why. By default, logs are written to Docker's internal log mechanism, but they won't be persisted after the test run completes and the containers are removed. - -In order to persist logs, you can configure the log driver to use. You can do this by setting the environment variable `LOG_DRIVER` before running the tests. On Linux/Unix systems, you can use the driver `journald`, as follows: - -```shell -# need to rebuild the test runner with the proper log driver -LOG_DRIVER=journald ./scripts.sh rebuild_test_runner - -# run the tests -./scripts.sh test - -# check the logs -journalctl -eu docker CONTAINER_NAME=jwt-nginx-test -``` - -Now you'll be able to see logs from previous test runs. The best way to make use of this is to open two terminals, one where you run the tests, and one where you follow the logs: - -```shell -# terminal 1 -./scripts.sh test - -# terminal 2 -journalctl -fu docker CONTAINER_NAME=jwt-nginx-test -``` +This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. It supports additional features such as extracting claims from the JWT and placing them on the request/response headers. ## Dependencies This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt). Transitively, that library depends on a JSON Parser called [Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. -## NGINX Directives +## Directives + This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. -| Directive | Description | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `auth_jwt_key` | The key to use to decode/verify the JWT, *in binhex format* -- see below. | -| `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | -| `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | -| `auth_jwt_enabled` | Set to "on" to enable JWT checking. | -| `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | -| `auth_jwt_extract_sub` | Set to "on" to extract the `sub` claim (e.g. user id) from the JWT and into the `x-userid` header on the response. | -| `auth_jwt_validate_email` | Set to "on" to extract the `emailAddress` claim from the JWT and into the `x-email` header on the response. | -| `auth_jwt_use_keyfile` | Set to "on" to read the key from a file rather than from the `auth_jwt_key` directive. | -| `auth_jwt_keyfile_path` | Set to the path from which the key should be read when `auth_jwt_use_keyfile` is enabled. | +| Directive | Description | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `auth_jwt_key` | The key to use to decode/verify the JWT, *in binhex format* -- see below. | +| `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | +| `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | +| `auth_jwt_enabled` | Set to "on" to enable JWT checking. | +| `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | +| `auth_jwt_validation_type` | Indicates where the JWT is located in the request -- see below. | +| `auth_jwt_validate_sub` | Set to "on" to validate the `sub` claim (e.g. user id) in the JWT. | +| `auth_jwt_extract_request_claims` | Set to a space-delimited list of claims to extract from the JWT and set as request headers. These will be accessible via e.g: `$http_jwt_sub` | +| `auth_jwt_extract_response_claims` | Set to a space-delimited list of claims to extract from the JWT and set as response headers. These will be accessible via e.g: `$sent_http_jwt_sub` | +| `auth_jwt_use_keyfile` | Set to "on" to read the key from a file rather than from the `auth_jwt_key` directive. | +| `auth_jwt_keyfile_path` | Set to the path from which the key should be read when `auth_jwt_use_keyfile` is enabled. | + +## Algorithms The default algorithm is `HS256`, for symmetric key validation. When using one of the `HS*` algorithms, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see [NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final), Section 5.3.2 The HMAC Key. +### Additional Supported Algorithms + The configuration also supports RSA public key validation via (e.g.) `auth_jwt_algorithm RS256`. When using the `RS*` alhorithms, the `auth_jwt_key` field must be set to your public key **OR** `auth_jwt_use_keyfile` should be set to `on` and `auth_jwt_keyfile_path` should point to the public key on disk. NGINX won't start if `auth_jwt_use_keyfile` is set to `on` and a key file is not provided. When using an `RS*` algorithm with an inline key, be sure to set `auth_jwt_key` to the _public key_, rather than a PEM certificate. E.g.: -``` +```nginx auth_jwt_key "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0aPPpS7ufs0bGbW9+OFQ RvJwb58fhi2BuHMd7Ys6m8D1jHW/AhDYrYVZtUnA60lxwSJ/ZKreYOQMlNyZfdqA @@ -106,42 +49,76 @@ oQIDAQAB When using an `RS*` algorithm with a public key file, do as follows: -``` +```nginx auth_jwt_use_keyfile on; auth_jwt_keyfile_path "/path/to/pub_key.pem"; ``` -A typical use would be to specify the key and login URL at the `http` level, and then only turn JWT authentication on for the locations which you want to secure. Unauthorized requests result in a 302 "Moved Temporarily" response with the `Location` header set to the URL specified in the `auth_jwt_loginurl` directive, and a querystring parameter `return_url` whose value is the current / attempted URL. +A typical use case would be to specify the key and login URL at the `http` level, and then only turn JWT authentication on for the locations which you want to secure (or vice-versa). Unauthorized requests will result in a `302 Moved Temporarily` response with the `Location` header set to the URL specified in the `auth_jwt_loginurl` directive, and a querystring parameter `return_url` whose value is the current / attempted URL. If you prefer to return `401 Unauthorized` rather than redirect, you may turn `auth_jwt_redirect` off: -``` +```nginx auth_jwt_redirect off; ``` +## JWT Locations -By default the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT: +By default, the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT: -``` +```nginx auth_jwt_validation_type COOKIE=jwt; ``` -By default, the module will attempt to extract the `sub` claim (e.g. the user's id) from the JWT. If successful, the -value will be set in the `x-userid` HTTP header. An error will be logged if this option is enabled and the JWT does not -contain the `sub` claim. You may disable this option as follows: +## `sub` Validation + +Optionally, the module can validate that a `sub` claim (e.g. the user's id) exists in the JWT. You may enable this feature as follows: +```nginx +auth_jwt_validate_sub on; ``` -auth_jwt_extract_sub off + +## Extracting Claims from the JWT + +You may specify claims to be extracted from the JWT and placed on the request and/or response headers. This is especially handly because the claims will then also be available as NGINX variables. + +If you only wish to access a claim as an NGINX variable, you should use `auth_jwt_extract_request_claims` so that the claim does not end up being sent to the client as a response header. However, if you do want the claim to be sent to the client in the response, then use `auth_jwt_extract_response_claims` instead. + +### Using Request Claims + +For example, you could configure an NGINX location which redirects to the current user's profile. Suppose `sub=abc-123`, the configuration below would redirect to `/profile/abc-123`. + +```nginx +location /profile/me { + auth_jwt_extract_request_claims sub; + + return 301 /profile/$http_jwt_sub; +} ``` -By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the -session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different -user identifier property such as `sub`, set `auth_jwt_validate_email` to the value `off`. _Note that this flag may be -renamed to `auth_jwt_extract_email` in a future release._ You may disable this option as follows: +### Using Response Claims + +Response claims are used in the same way, with the only differences being: + - the variables are accessed via the `$sent_http_jwt_*` pattern, e.g. `$sent_http_jwt_sub`, and + - the headers are sent to the client. +### Extracting Multiple Claims + +You may extract multiple claims by specifying all claims as arguments to a single directive, or by supplying multiple directives. The following two examples are equivalent. + +```nginx +auth_jwt_extract_request_claims sub firstName lastName; ``` -auth_jwt_validate_email off; + +```nginx +auth_jwt_extract_request_claims sub; +auth_jwt_extract_request_claims firstName; +auth_jwt_extract_request_claims lastName; ``` +## Versioning + +This module has historically not been versioned, however, we are now starting to version the module in order to add clarity. We will add releases here in GitHub with additional details. In the future we may also publish pre-built modules for a selection of NGINX versions. + ## Contributing If you'd like to contribute to this repository, please first initiate the Git hooks by running `./.bin/init` (note the `.` before `bin`) -- this will ensure that tests are run before you push your changes. @@ -158,10 +135,10 @@ If you'd like to contribute to this repository, please first initiate the Git ho "name": "Linux", "includePath": [ "${workspaceFolder}/**", - "~/Projects/third-party/nginx/objs/**", - "~/Projects/third-party/nginx/src/**", - "~/Projects/third-party/libjwt/include/**", - "~/Projects/third-party/jansson/src/**" + "~/Projects/nginx/objs/**", + "~/Projects/nginx/src/**", + "~/Projects/libjwt/include/**", + "~/Projects/jansson/src/**" ], "defines": [], "compilerPath": "/usr/bin/clang", @@ -183,18 +160,83 @@ Note the `includePath` additions above -- please update them as appropriate. Nex 3. Update the `includePath` entires shown above to match the location you chose. 4. Enter the directory where you extracted NGINX and run: `./configure --with-compat` -### Cloning libjwt +#### Cloning libjwt 1. Clone this repository as follows (replace ``): `git clone git@github.com:benmcollins/libjwt.git 2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` 3. Update the `includePath` entires shown above to match the location you chose. -### Cloning lobjansson +#### Cloning libjansson 1. Clone this repository as follows (replace ``): `git clone git@github.com:akheron/jansson.git 2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` 3. Update the `includePath` entires shown above to match the location you chose. -### Verify Compliation +#### Verifing Compliation Once you save your changes to `.vscode/c_cpp_properties.json`, you should see that warnings and errors in the Problems panel go away, at least temprorarily. Hopfeully they don't come back, but if they do, make sure your include paths are set correctly. + +### Building and Testing + +The `./scripts.sh` file contains multiple commands to make things easy: + +| Command | Description | +| --------------------- | ----------------------------------------------------------------- | +| `build_module` | Builds the NGINX image. | +| `rebuild_module` | Re-builds the NGINX image. | +| `start_nginx` | Starts the NGINX container. | +| `stop_nginx` | Stops the NGINX container. | +| `cp_bin` | Copies the compiled binaries out of the NGINX container. | +| `build_test_runner` | Builds the images used by the test stack (uses Docker compose). | +| `rebuild_test_runner` | Re-builds the images used by the test stack. | +| `test` | Runs `test.sh` against the NGINX container (uses Docker compose). | +| `test_now` | Runs `test.sh` without rebuilding. | + +You can run multiple commands in sequence by separating them with a space, e.g.: + +```shell +./scripts.sh build_module test +``` + +To build the Docker images, module, start NGINX, and run the tests against, you can simply do: + +```shell +./scripts.sh all +``` + +When you make a change to the module run `./scripts.sh build_module test` to build a fresh module and run the tests. Note that `rebuild_module` is not often needed as `build_module` hashes the module's source files which will cause a cache miss while building the container, causing the module to be rebuilt. + +When you make a change to the test NGINX config or `test.sh`, run `./scripts.sh test` to run the tests. Similar to above, the test sources are hashed and the containers will be rebuilt as needed. + +The image produced with `./scripts.sh build_module` only differs from the official NGINX image in two ways: + - the JWT module itself, and + - the `nginx.conf` file is overwritten with our own. + +The tests use a customized NGINX image, distinct from the main image, as well as a test runner image. By running `./scripts.sh test`, the two test containers will be stood up via Docker compose, then they'll be started, and the tests will run. At the end of the test run, both containers will be automatically stopped and destroyed. See below to learn how to trace test failures across runs. + +#### Tracing Test Failures + +After making changes and finding that some tests fail, it can be difficult to understand why. By default, logs are written to Docker's internal log mechanism, but they won't be persisted after the test run completes and the containers are removed. + +In order to persist logs, you can configure the log driver to use. You can do this by setting the environment variable `LOG_DRIVER` before running the tests. On Linux/Unix systems, you can use the driver `journald`, as follows: + +```shell +# need to rebuild the test runner with the proper log driver +LOG_DRIVER=journald ./scripts.sh rebuild_test_runner + +# run the tests +./scripts.sh test + +# check the logs +journalctl -eu docker CONTAINER_NAME=jwt-nginx-test +``` + +Now you'll be able to see logs from previous test runs. The best way to make use of this is to open two terminals, one where you run the tests, and one where you follow the logs: + +```shell +# terminal 1 +./scripts.sh test + +# terminal 2 +journalctl -fu docker CONTAINER_NAME=jwt-nginx-test +``` diff --git a/scripts.sh b/scripts.sh index 89ec2ba..873d170 100755 --- a/scripts.sh +++ b/scripts.sh @@ -12,44 +12,39 @@ export CONTAINER_NAME_PREFIX=${CONTAINER_NAME_PREFIX:-jwt-nginx-test} export NGINX_VERSION=${NGINX_VERSION:-1.22.0} all() { - build_nginx - start_nginx + build_module + build_test_runner test } -fetch_headers() { - printf "${BLUE} Fetching NGINX headers...${NC}" - local files='src/core/ngx_core.h src/http/ngx_http.h' - - for f in ${files}; do - curl "https://raw.githubusercontent.com/nginx/nginx/release-${NGINX_VERSION}/${f}" -o src/lib/$(basename ${f}) - done -} - -build_nginx() { +build_module() { local dockerArgs=${1:-} + local sourceHash=$(get_hash config src/*) - printf "${BLUE} Building NGINX...${NC}" + printf "${BLUE}Pulling images...${NC}\n" docker image pull debian:bullseye-slim docker image pull nginx:${NGINX_VERSION} - docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} --build-arg NGINX_VERSION=${NGINX_VERSION} ${dockerArgs} . + + printf "${BLUE}Building module...${NC}\n" + docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} ${dockerArgs} \ + --build-arg NGINX_VERSION=${NGINX_VERSION} \ + --build-arg SOURCE_HASH=${sourceHash} \. if [ "$?" -ne 0 ]; then - printf "${RED} Build failed ${NC}" + printf "${RED}✘ Build failed ${NC}\n" else - printf "${GREEN}✓ Successfully built NGINX module ${NC}" + printf "${GREEN}✔ Successfully built NGINX module ${NC}\n" fi - docker rmi -f $(docker images --filter=label=stage=builder --quiet) || true + docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true } -rebuild_nginx() { - printf "${BLUE} Rebuilding NGINX...${NC}" - build_nginx --no-cache +rebuild_module() { + build_module --no-cache } start_nginx() { - printf "${BLUE} Starting NGINX...${NC}" + printf "${BLUE}Starting NGINX...${NC}\n" docker run --rm --name "${IMAGE_NAME}" -d -p 8000:80 ${FULL_IMAGE_NAME} } @@ -62,7 +57,7 @@ cp_bin() { start_nginx fi - printf "${BLUE} Copying binaries...${NC}" + printf "${BLUE}Copying binaries...${NC}\n" rm -rf bin mkdir bin docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ @@ -73,9 +68,13 @@ cp_bin() { build_test_runner() { local dockerArgs=${1:-} + local configHash=$(get_hash $(find test -type f -not -name 'test.sh' -not -name '*.yml' -not -name 'Dockerfile*')) + local sourceHash=$(get_hash test/test.sh) - printf "${BLUE} Building test runner...${NC}" - docker compose -f ./test/docker-compose-test.yml build ${dockerArgs} + printf "${BLUE}Building test runner...${NC}\n" + docker compose -f ./test/docker-compose-test.yml build ${dockerArgs} \ + --build-arg CONFIG_HASH=${configHash}\ + --build-arg SOURCE_HASH=${sourceHash} } rebuild_test_runner() { @@ -83,13 +82,35 @@ rebuild_test_runner() { } test() { - printf "${BLUE} Running tests...${NC}" + build_test_runner + + printf "${BLUE}Running tests...${NC}\n" docker compose -f ./test/docker-compose-test.yml up --no-start docker start ${CONTAINER_NAME_PREFIX} - docker start -a ${CONTAINER_NAME_PREFIX}-runner + + if [ "$(docker container inspect -f '{{.State.Running}}' ${CONTAINER_NAME_PREFIX})" != "true" ]; then + printf "${RED}Failed to start NGINX test container. See logs below:\n" + docker logs ${CONTAINER_NAME_PREFIX} + printf "${NC}\n" + else + docker start -a ${CONTAINER_NAME_PREFIX}-runner + fi + docker compose -f ./test/docker-compose-test.yml down } -for fn in $@; do - "$fn" -done +test_now() { + docker start -a ${CONTAINER_NAME_PREFIX}-runner +} + +get_hash() { + sha1sum $@ | sed -E 's|\s+|:|' | tr '\n' ' ' | sha1sum | head -c 40 +} + +if [ $# -eq 0 ]; then + all +else + for fn in "$@"; do + ${fn} + done +fi diff --git a/src/ngx_http_auth_jwt_binary_converters.c b/src/ngx_http_auth_jwt_binary_converters.c index 8aea970..8b60560 100644 --- a/src/ngx_http_auth_jwt_binary_converters.c +++ b/src/ngx_http_auth_jwt_binary_converters.c @@ -8,42 +8,56 @@ */ #include "ngx_http_auth_jwt_binary_converters.h" - #include -int hex_char_to_binary( char ch, char* ret ) +int hex_char_to_binary(char ch, char *ret) { - ch = tolower( ch ); - if( isdigit( ch ) ) + ch = tolower(ch); + + if (isdigit(ch)) + { *ret = ch - '0'; - else if( ch >= 'a' && ch <= 'f' ) - *ret = ( ch - 'a' ) + 10; - else if( ch >= 'A' && ch <= 'F' ) - *ret = ( ch - 'A' ) + 10; + } + else if (ch >= 'a' && ch <= 'f') + { + *ret = (ch - 'a') + 10; + } + else if (ch >= 'A' && ch <= 'F') + { + *ret = (ch - 'A') + 10; + } else - return *ret = 0; - return 1; + { + return -1; + } + + return 0; } -int hex_to_binary( const char* str, u_char* buf, int len ) +int hex_to_binary(const char *str, u_char *buf, int len) { - u_char - *cpy = buf; - char - low, - high; - int - odd = len % 2; - - if (odd) { + int odd = len % 2; + + if (odd) + { return -1; } - - for (int i = 0; i < len; i += 2) { - hex_char_to_binary( *(str + i), &high ); - hex_char_to_binary( *(str + i + 1 ), &low ); + else + { + u_char *cpy = buf; + char low; + char high; - *cpy++ = low | (high << 4); + for (int i = 0; i < len; i += 2) + { + if (hex_char_to_binary(*(str + i), &high) != 0 || hex_char_to_binary(*(str + i + 1), &low) != 0) + { + return -2; + } + + *cpy++ = low | (high << 4); + } + + return 0; } - return 0; } \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_header_processing.c b/src/ngx_http_auth_jwt_header_processing.c index e368c84..ff648b9 100644 --- a/src/ngx_http_auth_jwt_header_processing.c +++ b/src/ngx_http_auth_jwt_header_processing.c @@ -16,80 +16,73 @@ * Sample code from nginx. * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/?highlight=http%20settings */ -ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) +ngx_table_elt_t *search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) { - ngx_list_part_t *part; - ngx_table_elt_t *h; - ngx_uint_t i; + ngx_list_part_t *part; + ngx_table_elt_t *h; + ngx_uint_t i; - // Get the first part of the list. There is usual only one part. - part = &r->headers_in.headers.part; - h = part->elts; + // Get the first part of the list. There is usual only one part. + part = &r->headers_in.headers.part; + h = part->elts; - // Headers list array may consist of more than one part, so loop through all of it - for (i = 0; /* void */ ; i++) - { - if (i >= part->nelts) - { - if (part->next == NULL) - { - /* The last part, search is done. */ - break; - } + // Headers list array may consist of more than one part, so loop through all of it + for (i = 0; /* void */; ++i) + { + if (i >= part->nelts) + { + if (part->next == NULL) + { + /* The last part, search is done. */ + break; + } - part = part->next; - h = part->elts; - i = 0; - } + part = part->next; + h = part->elts; + i = 0; + } - //Just compare the lengths and then the names case insensitively. - if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) - { - /* This header doesn't match. */ - continue; - } + // Just compare the lengths and then the names case insensitively. + if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) + { + /* This header doesn't match. */ + continue; + } - /* - * Ta-da, we got one! - * Note, we've stopped the search at the first matched header - * while more then one header may match. - */ - return &h[i]; - } + /* + * Ta-da, we got one! + * Note, we've stopped the search at the first matched header + * while more then one header may match. + */ + return &h[i]; + } - /* No headers was found */ - return NULL; + /* No headers found */ + return NULL; } -/** - * Sample code from nginx - * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/#how-can-i-set-a-header - */ -ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) { - ngx_table_elt_t *h; +ngx_int_t set_request_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) +{ + return set_header(ngx_list_push(&r->headers_in.headers), key, value); +} - /* - All we have to do is just to allocate the header... - */ - h = ngx_list_push(&r->headers_out.headers); - if (h == NULL) { - return NGX_ERROR; - } +ngx_int_t set_response_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) +{ + return set_header(ngx_list_push(&r->headers_out.headers), key, value); +} - /* - ... setup the header key ... - */ +ngx_int_t set_header(ngx_table_elt_t *h, ngx_str_t *key, ngx_str_t *value) +{ + if (h == NULL) + { + return NGX_ERROR; + } + else + { h->key = *key; - - /* - ... and the value. - */ h->value = *value; - - /* - Mark the header as not deleted. - */ h->hash = 1; return NGX_OK; + } } \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_header_processing.h b/src/ngx_http_auth_jwt_header_processing.h index 0b64133..acd1762 100644 --- a/src/ngx_http_auth_jwt_header_processing.h +++ b/src/ngx_http_auth_jwt_header_processing.h @@ -9,6 +9,8 @@ #define _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); -ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t set_request_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t set_response_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t set_header(ngx_table_elt_t *h, ngx_str_t *key, ngx_str_t *value); #endif /* _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H */ \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 508d781..3ecb0d3 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -20,543 +20,638 @@ #include -typedef struct { - ngx_str_t auth_jwt_loginurl; - ngx_str_t auth_jwt_key; - ngx_flag_t auth_jwt_enabled; - ngx_flag_t auth_jwt_redirect; - ngx_str_t auth_jwt_validation_type; - ngx_str_t auth_jwt_algorithm; - ngx_flag_t auth_jwt_extract_sub; - ngx_flag_t auth_jwt_validate_email; - ngx_str_t auth_jwt_keyfile_path; - ngx_flag_t auth_jwt_use_keyfile; - // Private field for keyfile data - ngx_str_t _auth_jwt_keyfile; -} ngx_http_auth_jwt_loc_conf_t; - -static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf); -static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r); -static void * ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf); -static char * ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); -static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type); - -static ngx_command_t ngx_http_auth_jwt_commands[] = { - - { ngx_string("auth_jwt_loginurl"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_loginurl), - NULL }, - - { ngx_string("auth_jwt_key"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_key), - NULL }, - - { ngx_string("auth_jwt_enabled"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_enabled), - NULL }, - - { ngx_string("auth_jwt_redirect"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_redirect), - NULL }, - - { ngx_string("auth_jwt_validation_type"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validation_type), - NULL }, - - { ngx_string("auth_jwt_algorithm"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_algorithm), - NULL }, - - { ngx_string("auth_jwt_extract_sub"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_extract_sub), - NULL }, - - { ngx_string("auth_jwt_validate_email"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validate_email), - NULL }, - - { ngx_string("auth_jwt_keyfile_path"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_keyfile_path), - NULL }, - - { ngx_string("auth_jwt_use_keyfile"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_use_keyfile), - NULL }, - - ngx_null_command +typedef struct +{ + ngx_str_t loginurl; + ngx_str_t key; + ngx_flag_t enabled; + ngx_flag_t redirect; + ngx_str_t validation_type; + ngx_str_t algorithm; + ngx_flag_t validate_sub; + ngx_array_t *extract_request_claims; + ngx_array_t *extract_response_claims; + ngx_str_t keyfile_path; + ngx_flag_t use_keyfile; + ngx_str_t _keyfile; +} auth_jwt_conf_t; + +static ngx_int_t init(ngx_conf_t *cf); +static void *create_conf(ngx_conf_t *cf); +static char *merge_conf(ngx_conf_t *cf, void *parent, void *child); +static char *merge_extract_request_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static ngx_int_t handle_request(ngx_http_request_t *r); +static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static int validate_exp(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static void extract_request_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static ngx_int_t free_jwt_and_redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf); +static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf); +static char *get_jwt(ngx_http_request_t *r, ngx_str_t validation_type); + +static char *JWT_HEADER_PREFIX = "JWT-"; + +static ngx_command_t auth_jwt_directives[] = { + {ngx_string("auth_jwt_loginurl"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, loginurl), + NULL}, + + {ngx_string("auth_jwt_key"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, key), + NULL}, + + {ngx_string("auth_jwt_enabled"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, enabled), + NULL}, + + {ngx_string("auth_jwt_redirect"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, redirect), + NULL}, + + {ngx_string("auth_jwt_validation_type"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, validation_type), + NULL}, + + {ngx_string("auth_jwt_algorithm"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, algorithm), + NULL}, + + {ngx_string("auth_jwt_validate_sub"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, validate_sub), + NULL}, + + {ngx_string("auth_jwt_extract_request_claims"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, + merge_extract_request_claims, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, extract_request_claims), + NULL}, + + {ngx_string("auth_jwt_extract_response_claims"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, + merge_extract_response_claims, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, extract_response_claims), + NULL}, + + {ngx_string("auth_jwt_keyfile_path"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, keyfile_path), + NULL}, + + {ngx_string("auth_jwt_use_keyfile"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, use_keyfile), + NULL}, + + ngx_null_command}; + +static ngx_http_module_t auth_jwt_context = { + NULL, /* preconfiguration */ + init, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + create_conf, /* create location configuration */ + merge_conf /* merge location configuration */ }; +ngx_module_t ngx_http_auth_jwt_module = { + NGX_MODULE_V1, + &auth_jwt_context, /* module context */ + auth_jwt_directives, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING}; + +static ngx_int_t init(ngx_conf_t *cf) +{ + ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + ngx_http_handler_pt *h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); + + if (h == NULL) + { + return NGX_ERROR; + } + else + { + *h = handle_request; + + return NGX_OK; + } +} -static ngx_http_module_t ngx_http_auth_jwt_module_ctx = { - NULL, /* preconfiguration */ - ngx_http_auth_jwt_init, /* postconfiguration */ +static void *create_conf(ngx_conf_t *cf) +{ + auth_jwt_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(auth_jwt_conf_t)); + + if (conf == NULL) + { + return NULL; + } + else + { + // ngx_str_t fields are initialized by the ngx_palloc call above -- only need to init flags and arrays here + conf->enabled = NGX_CONF_UNSET; + conf->redirect = NGX_CONF_UNSET; + conf->validate_sub = NGX_CONF_UNSET; + conf->redirect = NGX_CONF_UNSET; + conf->validate_sub = NGX_CONF_UNSET; + conf->extract_request_claims = NULL; + conf->extract_response_claims = NULL; + conf->use_keyfile = NGX_CONF_UNSET; + + return conf; + } +} - NULL, /* create main configuration */ - NULL, /* init main configuration */ +static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) +{ + const auth_jwt_conf_t *prev = parent; + auth_jwt_conf_t *conf = child; + + ngx_conf_merge_str_value(conf->loginurl, prev->loginurl, ""); + ngx_conf_merge_str_value(conf->key, prev->key, ""); + ngx_conf_merge_str_value(conf->validation_type, prev->validation_type, ""); + ngx_conf_merge_str_value(conf->algorithm, prev->algorithm, "HS256"); + ngx_conf_merge_str_value(conf->keyfile_path, prev->keyfile_path, ""); + ngx_conf_merge_off_value(conf->validate_sub, prev->validate_sub, 0); + ngx_conf_merge_ptr_value(conf->extract_request_claims, prev->extract_request_claims, NULL); + ngx_conf_merge_ptr_value(conf->extract_request_claims, prev->extract_response_claims, NULL); + + if (conf->enabled == NGX_CONF_UNSET) + { + conf->enabled = prev->enabled == NGX_CONF_UNSET ? 0 : prev->enabled; + } + + if (conf->redirect == NGX_CONF_UNSET) + { + conf->redirect = prev->redirect == NGX_CONF_UNSET ? 0 : prev->redirect; + } + + if (conf->use_keyfile == NGX_CONF_UNSET) + { + conf->use_keyfile = prev->use_keyfile == NGX_CONF_UNSET ? 0 : prev->use_keyfile; + } + + // If the usage of the keyfile is specified, check if the key_path is also configured + if (conf->use_keyfile == 1) + { + if (ngx_strcmp(conf->keyfile_path.data, "") != 0) + { + if (load_public_key(cf, conf) != NGX_OK) + { + return NGX_CONF_ERROR; + } + } + else + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "keyfile_path not specified"); + + return NGX_CONF_ERROR; + } + } + + return NGX_CONF_OK; +} - NULL, /* create server configuration */ - NULL, /* merge server configuration */ +static char *merge_extract_claims(ngx_conf_t *cf, ngx_array_t *claims) +{ + ngx_str_t *values = cf->args->elts; - ngx_http_auth_jwt_create_loc_conf, /* create location configuration */ - ngx_http_auth_jwt_merge_loc_conf /* merge location configuration */ -}; + // start at 1 because the first element is the directive (auth_jwt_extract_X_claims) + for (ngx_uint_t i = 1; i < cf->args->nelts; ++i) + { + ngx_str_t *element = ngx_array_push(claims); + *element = values[i]; + } -ngx_module_t ngx_http_auth_jwt_module = { - NGX_MODULE_V1, - &ngx_http_auth_jwt_module_ctx, /* module context */ - ngx_http_auth_jwt_commands, /* module directives */ - NGX_HTTP_MODULE, /* module type */ - NULL, /* init master */ - NULL, /* init module */ - NULL, /* init process */ - NULL, /* init thread */ - NULL, /* exit thread */ - NULL, /* exit process */ - NULL, /* exit master */ - NGX_MODULE_V1_PADDING -}; + return NGX_CONF_OK; +} + +static char *merge_extract_request_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c) +{ + auth_jwt_conf_t *conf = c; + ngx_array_t *claims = conf->extract_request_claims; + + if (claims == NULL) + { + claims = ngx_array_create(cf->pool, 1, sizeof(ngx_str_t)); + conf->extract_request_claims = claims; + } + + return merge_extract_claims(cf, claims); +} +static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c) +{ + auth_jwt_conf_t *conf = c; + ngx_array_t *claims = conf->extract_response_claims; + + if (claims == NULL) + { + claims = ngx_array_create(cf->pool, 1, sizeof(ngx_str_t)); + conf->extract_response_claims = claims; + } + + return merge_extract_claims(cf, claims); +} + +static ngx_int_t handle_request(ngx_http_request_t *r) +{ + auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); + + if (!jwtcf->enabled) + { + return NGX_DECLINED; + } + else + { + // pass through options requests without token authentication + if (r->method == NGX_HTTP_OPTIONS) + { + return NGX_DECLINED; + } + else + { + char *jwtPtr = get_jwt(r, jwtcf->validation_type); + + if (jwtPtr == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a JWT"); + return redirect(r, jwtcf); + } + else + { + ngx_str_t algorithm = jwtcf->algorithm; + int keyLength; + u_char *key; + jwt_t *jwt = NULL; + + if (algorithm.len == 0 || (algorithm.len == 5 && ngx_strncmp(algorithm.data, "HS", 2) == 0)) + { + keyLength = jwtcf->key.len / 2; + key = ngx_palloc(r->pool, keyLength); + + if (0 != hex_to_binary((char *)jwtcf->key.data, key, jwtcf->key.len)) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); + return redirect(r, jwtcf); + } + } + else if (algorithm.len == 5 && ngx_strncmp(algorithm.data, "RS", 2) == 0) + { + if (jwtcf->use_keyfile == 1) + { + keyLength = jwtcf->_keyfile.len; + key = (u_char *)jwtcf->_keyfile.data; + } + else + { + keyLength = jwtcf->key.len; + key = jwtcf->key.data; + } + } + else + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm %s", algorithm); + return redirect(r, jwtcf); + } + + if (jwt_decode(&jwt, jwtPtr, key, keyLength) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT"); + return redirect(r, jwtcf); + } + + if (validate_alg(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm specified"); + return free_jwt_and_redirect(r, jwtcf, jwt); + } + else if (validate_exp(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); + return free_jwt_and_redirect(r, jwtcf, jwt); + } + else if (validate_sub(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); + return free_jwt_and_redirect(r, jwtcf, jwt); + } + else + { + extract_request_claims(r, jwtcf, jwt); + extract_response_claims(r, jwtcf, jwt); + jwt_free(jwt); + + return NGX_OK; + } + } + } + } +} -static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) +static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { - ngx_str_t useridHeaderName = ngx_string("x-userid"); - ngx_str_t emailHeaderName = ngx_string("x-email"); - char* jwtPtr; - char* return_url; - ngx_http_auth_jwt_loc_conf_t *jwtcf; - u_char *keyBinary; - // For clearing it later on - jwt_t *jwt = NULL; - int jwtParseReturnCode; - jwt_alg_t alg; - time_t exp; - time_t now; - ngx_str_t auth_jwt_algorithm; - int keylen; - - jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); - - if (!jwtcf->auth_jwt_enabled) - { - return NGX_DECLINED; - } - - // pass through options requests without token authentication - if (r->method == NGX_HTTP_OPTIONS) - { - return NGX_DECLINED; - } - - jwtPtr = getJwt(r, jwtcf->auth_jwt_validation_type); - - if (jwtPtr == NULL) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a JWT"); - goto redirect; - } - - // convert key from hex to binary, if a symmetric key - - auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; - - if (auth_jwt_algorithm.len == 0 || (auth_jwt_algorithm.len == 5 && ngx_strncmp(auth_jwt_algorithm.data, "HS", 2) == 0)) - { - keylen = jwtcf->auth_jwt_key.len / 2; - keyBinary = ngx_palloc(r->pool, keylen); - if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); - goto redirect; - } - } - else if ( auth_jwt_algorithm.len == 5 && ngx_strncmp(auth_jwt_algorithm.data, "RS", 2) == 0 ) - { - // in this case, 'Binary' is a misnomer, as it is the public key string itself - if (jwtcf->auth_jwt_use_keyfile == 1) - { - // Set to global variables - // NOTE: check for keyBin == NULL skipped, unnecessary check; nginx should fail to start - keyBinary = (u_char*)jwtcf->_auth_jwt_keyfile.data; - keylen = jwtcf->_auth_jwt_keyfile.len; - } - else - { - keyBinary = jwtcf->auth_jwt_key.data; - keylen = jwtcf->auth_jwt_key.len; - } - } - else - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm %s", auth_jwt_algorithm); - goto redirect; - } - - // validate the jwt - jwtParseReturnCode = jwt_decode(&jwt, jwtPtr, keyBinary, keylen); - - if (jwtParseReturnCode != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT, error code %d", jwtParseReturnCode); - goto redirect; - } - - // validate the algorithm - alg = jwt_get_alg(jwt); - - if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in JWT (%d)", alg); - goto redirect; - } - - // validate the exp date of the JWT - exp = (time_t)jwt_get_grant_int(jwt, "exp"); - now = time(NULL); - - if (exp < now) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); - goto redirect; - } - - // extract the userid - if (jwtcf->auth_jwt_extract_sub == 1) - { - const char* sub = jwt_get_grant(jwt, "sub"); - - if (sub == NULL) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); - } - else - { - ngx_str_t sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); - - set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); - } - } - - if (jwtcf->auth_jwt_validate_email == 1) - { - const char* email = jwt_get_grant(jwt, "emailAddress"); - - if (email == NULL) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain an email address"); - } - else - { - ngx_str_t email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); - - set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); - } - } - - jwt_free(jwt); - - return NGX_OK; - - redirect: - if (jwt) - { - jwt_free(jwt); - } - - if (jwtcf->auth_jwt_redirect) - { - r->headers_out.location = ngx_list_push(&r->headers_out.headers); - - if (r->headers_out.location == NULL) - { - ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); - } - - r->headers_out.location->hash = 1; - r->headers_out.location->key.len = sizeof("Location") - 1; - r->headers_out.location->key.data = (u_char *) "Location"; - - if (r->method == NGX_HTTP_GET) - { - int loginlen; - char * scheme; - ngx_str_t server; - ngx_str_t uri_variable_name = ngx_string("request_uri"); - ngx_int_t uri_variable_hash; - ngx_http_variable_value_t * request_uri_var; - ngx_str_t uri; - ngx_str_t uri_escaped; - uintptr_t escaped_len; - - loginlen = jwtcf->auth_jwt_loginurl.len; - scheme = (r->connection->ssl) ? "https" : "http"; - server = r->headers_in.server; - - // get the URI - uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); - request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); - - // get the URI - if(request_uri_var && !request_uri_var->not_found && request_uri_var->valid) - { - // ideally we would like the uri with the querystring parameters - uri.data = ngx_palloc(r->pool, request_uri_var->len); - uri.len = request_uri_var->len; - ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); - } - else - { - // fallback to the querystring without params - uri = r->uri; - } - - // escape the URI - escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; - uri_escaped.data = ngx_palloc(r->pool, escaped_len); - uri_escaped.len = escaped_len; - ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); - - r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; - return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); - ngx_memcpy(return_url, jwtcf->auth_jwt_loginurl.data, jwtcf->auth_jwt_loginurl.len); - int return_url_idx = jwtcf->auth_jwt_loginurl.len; - ngx_memcpy(return_url+return_url_idx, "?return_url=", sizeof("?return_url=") - 1); - return_url_idx += sizeof("?return_url=") - 1; - ngx_memcpy(return_url+return_url_idx, scheme, strlen(scheme)); - return_url_idx += strlen(scheme); - ngx_memcpy(return_url+return_url_idx, "://", sizeof("://") - 1); - return_url_idx += sizeof("://") - 1; - ngx_memcpy(return_url+return_url_idx, server.data, server.len); - return_url_idx += server.len; - ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); - return_url_idx += uri_escaped.len; - r->headers_out.location->value.data = (u_char *)return_url; - } - else - { - // for non-get requests, redirect to the login page without a return URL - r->headers_out.location->value.len = jwtcf->auth_jwt_loginurl.len; - r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; - } - - return NGX_HTTP_MOVED_TEMPORARILY; - } - - // When no redirect is needed, no "Location" header construction is needed, and we can respond with a 401 - return NGX_HTTP_UNAUTHORIZED; + const jwt_alg_t alg = jwt_get_alg(jwt); + + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512) + { + return 1; + } + + return 0; } +static int validate_exp(auth_jwt_conf_t *jwtcf, jwt_t *jwt) +{ + const time_t exp = (time_t)jwt_get_grant_int(jwt, "exp"); + const time_t now = time(NULL); + + if (exp < now) + { + return 1; + } + + return 0; +} -static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf) +static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { - ngx_http_handler_pt *h; - ngx_http_core_main_conf_t *cmcf; + if (jwtcf->validate_sub == 1) + { + const char *sub = jwt_get_grant(jwt, "sub"); - cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + if (sub == NULL) + { + return 1; + } + } - h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); - if (h == NULL) - { - return NGX_ERROR; - } + return 0; +} - *h = ngx_http_auth_jwt_handler; +static void extract_claims(ngx_http_request_t *r, jwt_t *jwt, ngx_array_t *claims, ngx_int_t (*set_header)(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value)) +{ + if (claims != NULL && claims->nelts > 0) + { + const ngx_str_t *claimsPtr = claims->elts; + + for (uint i = 0; i < claims->nelts; ++i) + { + const ngx_str_t claim = claimsPtr[i]; + const char *value = jwt_get_grant(jwt, (char *)claim.data); + + if (value != NULL && strlen(value) > 0) + { + ngx_uint_t claimHeaderLen = strlen(JWT_HEADER_PREFIX) + claim.len; + ngx_str_t claimHeader = ngx_null_string; + ngx_str_t claimValue = char_ptr_to_ngx_str_t(r->pool, value); + + claimHeader.data = ngx_palloc(r->pool, claimHeaderLen); + claimHeader.len = claimHeaderLen; + ngx_snprintf(claimHeader.data, claimHeaderLen, "%s%V", JWT_HEADER_PREFIX, &claim); + + set_header(r, &claimHeader, &claimValue); + } + } + } +} - return NGX_OK; +static void extract_request_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) +{ + extract_claims(r, jwt, jwtcf->extract_request_claims, set_request_header); } -static void * -ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) +static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) { - ngx_http_auth_jwt_loc_conf_t *conf; - - conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_jwt_loc_conf_t)); - if (conf == NULL) - { - return NULL; - } - - // set the flag to unset - conf->auth_jwt_enabled = (ngx_flag_t) -1; - conf->auth_jwt_redirect = (ngx_flag_t) -1; - conf->auth_jwt_extract_sub = (ngx_flag_t) -1; - conf->auth_jwt_validate_email = (ngx_flag_t) -1; - conf->auth_jwt_use_keyfile = (ngx_flag_t) -1; - - ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); - - return conf; + extract_claims(r, jwt, jwtcf->extract_response_claims, set_response_header); } -// Loads the RSA256 public key into the location config struct -static ngx_int_t -loadAuthKey(ngx_conf_t *cf, ngx_http_auth_jwt_loc_conf_t* conf) { - FILE *keyFile = fopen((const char*)conf->auth_jwt_keyfile_path.data, "rb"); - unsigned long keySize; - unsigned long keySizeRead; - - // Check if file exists or is correctly opened - if (keyFile == NULL) - { - ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to open public key file"); - return NGX_ERROR; - } - - // Read file length - fseek(keyFile, 0, SEEK_END); - keySize = ftell(keyFile); - fseek(keyFile, 0, SEEK_SET); - - if (keySize == 0) - { - ngx_log_error(NGX_LOG_ERR, cf->log, 0, "invalid public key file size of 0"); - return NGX_ERROR; - } - - conf->_auth_jwt_keyfile.data = ngx_palloc(cf->pool, keySize); - keySizeRead = fread(conf->_auth_jwt_keyfile.data, 1, keySize, keyFile); - fclose(keyFile); - - if (keySizeRead == keySize) - { - conf->_auth_jwt_keyfile.len = (int)keySize; - - return NGX_OK; - } - else { - ngx_log_error(NGX_LOG_ERR, cf->log, 0, "public key size %i does not match expected size of %i", keySizeRead, keySize); - - return NGX_ERROR; - } +static ngx_int_t free_jwt_and_redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) +{ + if (jwt) + { + jwt_free(jwt); + } + + return redirect(r, jwtcf); } -static char * -ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) +static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) { - ngx_http_auth_jwt_loc_conf_t *prev = parent; - ngx_http_auth_jwt_loc_conf_t *conf = child; - - ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); - ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); - ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); - ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); - ngx_conf_merge_str_value(conf->auth_jwt_keyfile_path, prev->auth_jwt_keyfile_path, ""); - ngx_conf_merge_off_value(conf->auth_jwt_extract_sub, prev->auth_jwt_extract_sub, 1); - ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); - - if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) - { - conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; - } - - if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) - { - conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; - } - - if (conf->auth_jwt_use_keyfile == ((ngx_flag_t) -1)) - { - conf->auth_jwt_use_keyfile = (prev->auth_jwt_use_keyfile == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_use_keyfile; - } - - // If the usage of the keyfile is specified, check if the key_path is also configured - if (conf->auth_jwt_use_keyfile == 1) - { - if (ngx_strcmp(conf->auth_jwt_keyfile_path.data, "") != 0) - { - if (loadAuthKey(cf, conf) != NGX_OK) - return NGX_CONF_ERROR; - } - else - { - ngx_log_error(NGX_LOG_ERR, cf->log, 0, "auth_jwt_keyfile_path not specified"); - return NGX_CONF_ERROR; - } - } - - return NGX_CONF_OK; + if (jwtcf->redirect) + { + r->headers_out.location = ngx_list_push(&r->headers_out.headers); + + if (r->headers_out.location == NULL) + { + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + } + + r->headers_out.location->hash = 1; + r->headers_out.location->key.len = sizeof("Location") - 1; + r->headers_out.location->key.data = (u_char *)"Location"; + + if (r->method == NGX_HTTP_GET) + { + const int loginlen = jwtcf->loginurl.len; + const char *scheme = (r->connection->ssl) ? "https" : "http"; + const ngx_str_t server = r->headers_in.server; + ngx_str_t uri_variable_name = ngx_string("request_uri"); + ngx_int_t uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); + ngx_http_variable_value_t *request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); + ngx_str_t uri; + ngx_str_t uri_escaped; + uintptr_t escaped_len; + char *return_url; + int return_url_idx; + + // get the URI + if (request_uri_var && !request_uri_var->not_found && request_uri_var->valid) + { + // ideally we would like the URI with the querystring parameters + uri.data = ngx_palloc(r->pool, request_uri_var->len); + uri.len = request_uri_var->len; + ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); + } + else + { + // fallback to the querystring without params + uri = r->uri; + } + + // escape the URI + escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; + uri_escaped.data = ngx_palloc(r->pool, escaped_len); + uri_escaped.len = escaped_len; + ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); + + r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; + + return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); + ngx_memcpy(return_url, jwtcf->loginurl.data, jwtcf->loginurl.len); + + return_url_idx = jwtcf->loginurl.len; + ngx_memcpy(return_url + return_url_idx, "?return_url=", sizeof("?return_url=") - 1); + + return_url_idx += sizeof("?return_url=") - 1; + ngx_memcpy(return_url + return_url_idx, scheme, strlen(scheme)); + + return_url_idx += strlen(scheme); + ngx_memcpy(return_url + return_url_idx, "://", sizeof("://") - 1); + + return_url_idx += sizeof("://") - 1; + ngx_memcpy(return_url + return_url_idx, server.data, server.len); + + return_url_idx += server.len; + ngx_memcpy(return_url + return_url_idx, uri_escaped.data, uri_escaped.len); + + r->headers_out.location->value.data = (u_char *)return_url; + } + else + { + // for non-get requests, redirect to the login page without a return URL + r->headers_out.location->value.len = jwtcf->loginurl.len; + r->headers_out.location->value.data = jwtcf->loginurl.data; + } + + return NGX_HTTP_MOVED_TEMPORARILY; + } + + // When no redirect is needed, no "Location" header construction is needed, and we can respond with a 401 + return NGX_HTTP_UNAUTHORIZED; } -static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) +// Loads the public key into the location config struct +static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf) { - static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); - ngx_table_elt_t *authorizationHeader; - char* jwtPtr = NULL; - ngx_str_t jwtCookieVal; - ngx_int_t n; - ngx_int_t bearer_length; - ngx_str_t authorizationHeaderStr; - - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "auth_jwt_validation_type.len %d", auth_jwt_validation_type.len); - - if (auth_jwt_validation_type.len == 0 || (auth_jwt_validation_type.len == sizeof("AUTHORIZATION") - 1 && ngx_strncmp(auth_jwt_validation_type.data, "AUTHORIZATION", sizeof("AUTHORIZATION") - 1)==0)) - { - // using authorization header - authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); - if (authorizationHeader != NULL) - { - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); - - bearer_length = authorizationHeader->value.len - (sizeof("Bearer ") - 1); - - if (bearer_length > 0) - { - authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; - authorizationHeaderStr.len = bearer_length; - - jwtPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); - - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtPtr); - } - } - } - else if (auth_jwt_validation_type.len > sizeof("COOKIE=") && ngx_strncmp(auth_jwt_validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1)==0) - { - auth_jwt_validation_type.data += sizeof("COOKIE=") - 1; - auth_jwt_validation_type.len -= sizeof("COOKIE=") - 1; - - // get the cookie - // TODO: the cookie name could be passed in dynamicallly - n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &auth_jwt_validation_type, &jwtCookieVal); - if (n != NGX_DECLINED) - { - jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); - } - } - - return jwtPtr; + FILE *keyFile = fopen((const char *)conf->keyfile_path.data, "rb"); + + // Check if file exists or is correctly opened + if (keyFile == NULL) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to open public key file"); + return NGX_ERROR; + } + else + { + u_long keySize; + u_long keySizeRead; + + // Read file length + fseek(keyFile, 0, SEEK_END); + keySize = ftell(keyFile); + fseek(keyFile, 0, SEEK_SET); + + if (keySize == 0) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "invalid public key file size of 0"); + return NGX_ERROR; + } + else + { + conf->_keyfile.data = ngx_palloc(cf->pool, keySize); + keySizeRead = fread(conf->_keyfile.data, 1, keySize, keyFile); + fclose(keyFile); + + if (keySizeRead == keySize) + { + conf->_keyfile.len = (int)keySize; + + return NGX_OK; + } + else + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "public key size %i does not match expected size of %i", keySizeRead, keySize); + return NGX_ERROR; + } + } + } } +static char *get_jwt(ngx_http_request_t *r, ngx_str_t validation_type) +{ + char *jwtPtr = NULL; + + ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "validation_type.len %d", validation_type.len); + + if (validation_type.len == 0 || (validation_type.len == sizeof("AUTHORIZATION") - 1 && ngx_strncmp(validation_type.data, "AUTHORIZATION", sizeof("AUTHORIZATION") - 1) == 0)) + { + static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); + const ngx_table_elt_t *authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); + + if (authorizationHeader != NULL) + { + ngx_int_t bearer_length = authorizationHeader->value.len - (sizeof("Bearer ") - 1); + + ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); + if (bearer_length > 0) + { + ngx_str_t authorizationHeaderStr; + authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; + authorizationHeaderStr.len = bearer_length; + jwtPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); + + ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtPtr); + } + } + } + else if (validation_type.len > sizeof("COOKIE=") && ngx_strncmp(validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1) == 0) + { + ngx_int_t n; + ngx_str_t jwtCookieVal; + + validation_type.data += sizeof("COOKIE=") - 1; + validation_type.len -= sizeof("COOKIE=") - 1; + + n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &validation_type, &jwtCookieVal); + + if (n != NGX_DECLINED) + { + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); + } + } + + return jwtPtr; +} diff --git a/src/ngx_http_auth_jwt_string.c b/src/ngx_http_auth_jwt_string.c index 186121f..f472171 100644 --- a/src/ngx_http_auth_jwt_string.c +++ b/src/ngx_http_auth_jwt_string.c @@ -15,18 +15,22 @@ char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str) { char* char_ptr = ngx_palloc(pool, str.len + 1); ngx_memcpy(char_ptr, str.data, str.len); + *(char_ptr + str.len) = '\0'; + return char_ptr; } /** copies a character pointer string to an nginx string structure */ -ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr) +ngx_str_t char_ptr_to_ngx_str_t(ngx_pool_t *pool, const char* char_ptr) { - int len = strlen(char_ptr); - + const int len = strlen(char_ptr); ngx_str_t str_t; + + str_t.len = len; str_t.data = ngx_palloc(pool, len); + ngx_memcpy(str_t.data, char_ptr, len); - str_t.len = len; + return str_t; } \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_string.h b/src/ngx_http_auth_jwt_string.h index 594785b..4440d8b 100644 --- a/src/ngx_http_auth_jwt_string.h +++ b/src/ngx_http_auth_jwt_string.h @@ -13,6 +13,6 @@ #include char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); -ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr); +ngx_str_t char_ptr_to_ngx_str_t(ngx_pool_t *pool, const char* char_ptr); #endif /* _NGX_HTTP_AUTH_JWT_STRING_H */ \ No newline at end of file diff --git a/test/Dockerfile-test-nginx b/test/Dockerfile-test-nginx index c1e4550..5497d07 100644 --- a/test/Dockerfile-test-nginx +++ b/test/Dockerfile-test-nginx @@ -1,5 +1,9 @@ ARG BASE_IMAGE +ARG CONFIG_HASH FROM ${BASE_IMAGE} as NGINX -COPY test.conf /etc/nginx/conf.d/test.conf -COPY rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf \ No newline at end of file +ARG CONFIG_HASH +RUN echo "Config Hash: ${CONFIG_HASH}" +COPY /docker-entrypoint.d/* /docker-entrypoint.d/ +COPY /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.conf +COPY /etc/nginx/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf diff --git a/test/Dockerfile-test-runner b/test/Dockerfile-test-runner index bd9fc59..0992d75 100644 --- a/test/Dockerfile-test-runner +++ b/test/Dockerfile-test-runner @@ -1,4 +1,10 @@ -FROM alpine:3.7 -COPY test.sh . +ARG SOURCE_HASH + +FROM alpine:3.7 AS test-base RUN apk add curl bash + +FROM test-base AS test +ARG SOURCE_HASH +RUN echo "Source Hash: ${SOURCE_HASH}" +COPY test.sh . CMD ["./test.sh"] diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index 2607e42..eff2460 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -9,6 +9,7 @@ services: dockerfile: Dockerfile-test-nginx args: BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} + command: [nginx-debug, '-g', 'daemon off;'] logging: driver: ${LOG_DRIVER:-journald} diff --git a/test/docker-entrypoint.d/10-nginx-test.sh b/test/docker-entrypoint.d/10-nginx-test.sh new file mode 100755 index 0000000..0bf8791 --- /dev/null +++ b/test/docker-entrypoint.d/10-nginx-test.sh @@ -0,0 +1 @@ +nginx -t \ No newline at end of file diff --git a/test/test.conf b/test/etc/nginx/conf.d/test.conf similarity index 59% rename from test/test.conf rename to test/etc/nginx/conf.d/test.conf index 52c2829..fcc3900 100644 --- a/test/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -1,3 +1,6 @@ +error_log /var/log/nginx/debug.log debug; +access_log /var/log/nginx/access.log; + server { listen 8000; server_name localhost; @@ -19,6 +22,16 @@ server { alias /usr/share/nginx/html/; try_files index.html =404; } + + location /secure/cookie/default/validate-sub { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validate_sub on; + auth_jwt_validation_type COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } location /secure/cookie/default/no-redirect { auth_jwt_enabled on; @@ -130,5 +143,79 @@ BwIDAQAB alias /usr/share/nginx/html/; try_files index.html =404; } + + location /secure/extract-claim/request/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_extract_request_claims sub; + + add_header "Test" "sub=$http_jwt_sub"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/name-1 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_extract_request_claims firstName lastName; + + add_header "Test" "$http_jwt_firstname $http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/name-2 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_extract_request_claims firstName; + auth_jwt_extract_request_claims lastName; + + add_header "Test" "$http_jwt_firstname $http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_extract_response_claims sub; + + add_header "Test" "sub=$sent_http_jwt_sub"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/name-1 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_extract_response_claims firstName lastName; + + add_header "Test" "$sent_http_jwt_firstname $sent_http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/name-2 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_validation_type AUTHORIZATION; + auth_jwt_extract_response_claims firstName; + auth_jwt_extract_response_claims lastName; + + add_header "Test" "$sent_http_jwt_firstname $sent_http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } } diff --git a/test/rsa_key_2048-pub.pem b/test/etc/nginx/rsa_key_2048-pub.pem similarity index 100% rename from test/rsa_key_2048-pub.pem rename to test/etc/nginx/rsa_key_2048-pub.pem diff --git a/test/test.sh b/test/test.sh index b6ec12c..6ab567b 100755 --- a/test/test.sh +++ b/test/test.sh @@ -1,24 +1,91 @@ #!/bin/bash -RED='\033[01;31m' -GREEN='\033[01;32m' -NONE='\033[00m' +# set a test # here to execute only that test and output additional info +DEBUG= -run_test () { - local name=$1 - local path=$2 - local expect=$3 - local extra=$4 +RED='\e[31m' +GREEN='\e[32m' +GRAY='\e[90m' +NC='\e[00m' - cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://nginx:8000${path} -H 'cache-control: no-cache' $extra" - result=$(eval ${cmd}) +NUM_TESTS=0; +NUM_SKIPPED=0; +NUM_FAILED=0; - if [ "${result}" -eq "${expect}" ]; then - echo -e "${GREEN}${name}: passed (Received: ${result}; Path: ${path})${NONE}"; - return 0 +run_test () { + NUM_TESTS=$((${NUM_TESTS} + 1)); + + if [ "${DEBUG}" == '' ] || [ ${DEBUG} == ${NUM_TESTS} ]; then + local OPTIND; + local name='' + local path='' + local expectedCode='' + local expectedResponseRegex='' + local extraCurlOpts='' + local curlCommand='' + local exitCode='' + local response='' + local testNum="${GRAY}${NUM_TESTS}${NC}\t" + + while getopts "n:p:r:c:x:" option; do + case $option in + n) + name=$OPTARG;; + p) + path=$OPTARG;; + c) + expectedCode=$OPTARG;; + r) + expectedResponseRegex=$OPTARG;; + x) + extraCurlOpts=$OPTARG;; + \?) # Invalid option + printf "Error: Invalid option\n"; + exit;; + esac + done + + curlCommand="curl -s -v http://nginx:8000${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" + response=$(eval "${curlCommand}") + exitCode=$? + + printf "\n${testNum}" + + if [ "${exitCode}" -ne "0" ]; then + printf "${RED}${name} -- unexpected exit code from cURL\n\tcURL Exit Code: ${exitCode}"; + NUM_FAILED=$((${NUM_FAILED} + 1)); + else + OKAY=1 + + if [ "${expectedCode}" != "" ]; then + local responseCode=$(echo "${response}" | grep -Eo 'HTTP/1.1 ([0-9]{3})' | awk '{print $2}') + + if [ "${expectedCode}" != "${responseCode}" ]; then + printf "${RED}${name} -- unexpected status code\n\tExpected: ${expectedCode}\n\tActual: ${responseCode}\n\tPath: ${path}" + NUM_FAILED=$((${NUM_FAILED} + 1)) + OKAY=0 + fi + fi + + if [ "${OKAY}" == "1" ] && [ "${expectedResponseRegex}" != "" ] && echo "${response}" | grep -Eq "${expectedResponseRegex}"; then + printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex}" + NUM_FAILED=$((${NUM_FAILED} + 1)) + OKAY=0 + fi + + if [ "${OKAY}" == "1" ]; then + printf "${GREEN}${name}"; + fi + fi + + if [ "${DEBUG}" == "${NUM_TESTS}" ]; then + printf '\n\tcURL Command: %s' "${curlCommand:---}" + printf '\n\tResponse: %s' "${response:---}" + fi + + printf "${NC}\n" else - echo -e "${RED}${name}: failed (Expected: ${expect}; Received: ${result}; Path: ${path})${NONE}"; - return 1 + NUM_SKIPPED=$((${NUM_SKIPPED} + 1)) fi } @@ -32,108 +99,129 @@ main() { local JWT_RS256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 local JWT_RS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.H35bTcZRhepWIoa8pKCbUMRuAOkVX9K5hJjc6tPmQwWmTw8lrktsvmMzJg_rgqnJLnAkciSIQw5EDj7fngS5zX2ThyRxrkPuE2Uiyw2Ect-mo9Kg1lrWgnyZCuCgq-Up9HQRAv0160mePlm8Gs4TOY6CPr38zwTcDZsy_Keq93igDQV8WuuWAGICaGd5ZyUOPjjzGShRjTU8Szz7fnpZpTtYRCYVo0pc5yfRWYm0fdn-4AseyGvd8JJ2xfnAEe4kZOkz7X1MLKtL0slKg3m2PH1lD7HwxIawXRTPWxArhJ9dcTNiDUrqtde2juGwOuMD_zTsb2Jj0_rmRb0Q6aljNw local JWT_RS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.iUupyKypfXJ5aZWfItSW-mOmx9a4C4X7Yr5p5Fk8W75ZhkOq0EeNfstTxx870brhkdPovBhO2LYI44_HoH9XicQNL6JnFprE0r61eJFngbuzlhRQiWpq0xYrazJWc9zB7_GgL2ZCwtw-Ts3G23Q0632wVm6-d7MKvG7RS8aEjN-MuVGdtLglH3forpItmFxw-if40EQsBL7hncN_XNcQTO4KPHkqmlpac_oKXRrLFDIIt2tB6OOpvY4QcpERoxexp4pi2f-JoINnWX_dU5JnIs3ypVJLQPfoJvxg8fsg3zYrOvMYnfsqOCYoHtZGK0O7jyfFmcGo5v2hLT-CpoF3Zw - local num_tests=0 - local num_failed=0 - - run_test 'when auth disabled, should return 200' \ - '/' \ - '200' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ - '/secure/auth-header/default' \ - '302' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm with no redirect and Authroization header missing Bearer, should return 401' \ - '/secure/auth-header/default/no-redirect' \ - '401' \ - '--header "Authorization: X"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ - '/secure/cookie/default' \ - '302' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm with no redirect and no JWT cookie, should return 401' \ - '/secure/cookie/default/no-redirect' \ - '401' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm and valid JWT cookie, returns 200' \ - '/secure/cookie/default' \ - '200' \ - '--cookie "jwt=${JWT_HS256_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm and valid JWT cookie with no sub, returns 200' \ - '/secure/cookie/default' \ - '200' \ - ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with default algorithm and valid JWT cookie with no email, returns 200' \ - '/secure/cookie/default' \ - '200' \ - ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ - '/secure/cookie/hs256/' \ - '200' \ - '--cookie "jwt=${JWT_HS256_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with HS384 algorithm and valid JWT cookie, returns 200' \ - '/secure/cookie/hs384' \ - '200' \ - '--cookie "jwt=${JWT_HS384_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with HS512 algorithm and valid JWT cookie, returns 200' \ - '/secure/cookie/hs512' \ - '200' \ - '--cookie "jwt=${JWT_HS512_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with RS256 algorithm and valid JWT cookie, returns 200' \ - '/secure/cookie/rs256' \ - '200' \ - ' --cookie "jwt=${JWT_RS256_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ - '/secure/auth-header/rs256/file' \ - '200' \ - '--header "Authorization: Bearer ${JWT_RS256_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with RS256 algorithm via file and invalid JWT in Authorization header, returns 401' \ - '/secure/auth-header/rs256/file' \ - '302' \ - '--header "Authorization: Bearer ${JWT_RS256_INVALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with RS384 algorithm via file and valid JWT in Authorization header, returns 200' \ - '/secure/auth-header/rs384/file' \ - '200' \ - '--header "Authorization: Bearer ${JWT_RS256_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - run_test 'when auth enabled with RS512 algorithm via file and valid JWT in Authorization header, returns 200' \ - '/secure/auth-header/rs512/file' \ - '200' \ - '--header "Authorization: Bearer ${JWT_RS256_VALID}"' - num_failed=$((${num_failed} + $?)); num_tests=$((${num_tests} + 1)); - - if [[ "${num_failed}" = '0' ]]; then - printf "\nRan ${num_tests} tests successfully.\n" + + run_test -n 'when auth disabled, should return 200' \ + -p '/' \ + -c '200' + + run_test -n 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + -p '/secure/auth-header/default' \ + -c '302' + + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 401' \ + -p '/secure/auth-header/default/no-redirect' \ + -c '401' \ + -x '--header "Authorization: X"' + + run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ + -p '/secure/cookie/default' \ + -c '302' + + run_test -n 'when auth enabled with default algorithm with no redirect and no JWT cookie, should return 401' \ + -p '/secure/cookie/default/no-redirect' \ + -c '401' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x "--cookie jwt=${JWT_HS256_VALID}" + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub when sub validated, returns 302' \ + -p '/secure/cookie/default/validate-sub' \ + -c '302' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no email, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs256/' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS384 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs384' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS384_VALID}"' + + run_test -n 'when auth enabled with HS512 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs512' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS512_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/rs256' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs256/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + -p '/secure/auth-header/rs256/file' \ + -c '302' \ + -x '--header "Authorization: Bearer ${JWT_RS256_INVALID}"' + + run_test -n 'when auth enabled with RS384 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs384/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS512 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs512/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'extracts single claim to request header' \ + -p '/secure/extract-claim/request/sub' \ + -r '^Test: sub=some-long-uuid$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to request header' \ + -p '/secure/extract-claim/request/name-1' \ + -r '^Test: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to request header' \ + -p '/secure/extract-claim/request/name-2' \ + -r '^Test: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to response header' \ + -p '/secure/extract-claim/response/sub' \ + -r '^Test: sub=some-long-uuid$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to response header' \ + -p '/secure/extract-claim/response/name-1' \ + -r '^Test: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to response header' \ + -p '/secure/extract-claim/response/name-2' \ + -r '^Test: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + if [[ "${NUM_FAILED}" = '0' ]]; then + printf "\nRan ${NUM_TESTS} tests successfully (skipped ${NUM_SKIPPED}).\n" return 0 else - printf "\nRan ${num_tests} tests: ${GREEN}$((${num_tests} - ${num_failed})) passed${NONE}; ${RED}${num_failed} failed${NONE}\n" + printf "\nRan ${NUM_TESTS} tests: ${GREEN}$((${NUM_TESTS} - ${NUM_FAILED})) passed${NC}; ${RED}${NUM_FAILED} failed${NC}; ${NUM_SKIPPED} skipped\n" return 1 fi } -main '$@' +if [ "${DEBUG}" != '' ]; then + printf "\n${RED}Some tests will be skipped since DEBUG is set.${NC}\n" +fi + +main From ac147ef0e7873b2becb2e68cf65b89c3c2848bcf Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 24 Apr 2023 08:05:03 -0400 Subject: [PATCH 079/130] update Dockerfile; update scripts.sh (#88) --- Dockerfile | 40 +++++++++++++++++++++++----------------- scripts.sh | 34 +++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/Dockerfile b/Dockerfile index 328ea1e..94d829e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,38 +4,44 @@ ARG SOURCE_HASH FROM debian:bullseye-slim as ngx_http_auth_jwt_builder_base LABEL stage=ngx_http_auth_jwt_builder -RUN apt-get update &&\ - apt-get install -y curl build-essential - - +RUN <<` +apt-get update +apt-get install -y curl build-essential +` FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module LABEL stage=ngx_http_auth_jwt_builder ENV LD_LIBRARY_PATH=/usr/local/lib ARG NGINX_VERSION -RUN set -x &&\ - apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev &&\ - mkdir -p /root/build/ngx-http-auth-jwt-module +RUN <<` +apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev +mkdir -p /root/build/ngx-http-auth-jwt-module +` WORKDIR /root/build/ngx-http-auth-jwt-module ARG SOURCE_HASH RUN echo "Source Hash: ${SOURCE_HASH}" ADD config ./ ADD src/*.h src/*.c ./src/ WORKDIR /root/build -RUN set -x &&\ - mkdir nginx &&\ - curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz &&\ - tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx +RUN <<` +mkdir nginx +curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz +tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx +` WORKDIR /root/build/nginx -RUN ./configure --with-debug --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module &&\ - make modules +RUN <<` +./configure --with-debug --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module +make modules +` FROM nginx:${NGINX_VERSION} AS ngx_http_auth_jwt_builder_nginx LABEL stage= RUN rm /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh /etc/nginx/conf.d/default.conf -RUN apt-get update &&\ - apt-get -y install libjansson4 libjwt0 &&\ - cd /etc/nginx &&\ - sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf +RUN <<` +apt-get update +apt-get -y install libjansson4 libjwt0 +cd /etc/nginx +sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf +` LABEL maintainer="TeslaGov" email="developers@teslagov.com" COPY --from=ngx_http_auth_jwt_builder_module /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ diff --git a/scripts.sh b/scripts.sh index 873d170..e5a2380 100755 --- a/scripts.sh +++ b/scripts.sh @@ -28,42 +28,54 @@ build_module() { printf "${BLUE}Building module...${NC}\n" docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} ${dockerArgs} \ --build-arg NGINX_VERSION=${NGINX_VERSION} \ - --build-arg SOURCE_HASH=${sourceHash} \. + --build-arg SOURCE_HASH=${sourceHash} . if [ "$?" -ne 0 ]; then printf "${RED}✘ Build failed ${NC}\n" else printf "${GREEN}✔ Successfully built NGINX module ${NC}\n" fi - - docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true } rebuild_module() { + clean_module build_module --no-cache } +clean_module() { + docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true +} + start_nginx() { - printf "${BLUE}Starting NGINX...${NC}\n" - docker run --rm --name "${IMAGE_NAME}" -d -p 8000:80 ${FULL_IMAGE_NAME} + printf "${BLUE}Starting NGINX container (${IMAGE_NAME})...${NC}\n" + docker run --rm --name "${IMAGE_NAME}" -d -p 8000:80 ${FULL_IMAGE_NAME} >/dev/null } stop_nginx() { - docker stop "${IMAGE_NAME}" + docker stop "${IMAGE_NAME}" >/dev/null } cp_bin() { - if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME})" != "true" ]; then + local destDir=bin + local stopContainer=0; + + if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME} | true)" != "true" ]; then start_nginx + stopContainer=1 fi - printf "${BLUE}Copying binaries...${NC}\n" - rm -rf bin - mkdir bin + printf "${BLUE}Copying binaries to: ${destDir}${NC}\n" + rm -rf ${destDir}/* + mkdir -p ${destDir} docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ usr/lib/x86_64-linux-gnu/libjansson.so.* \ - usr/lib/x86_64-linux-gnu/libjwt.*" | tar -xf - -C bin &>/dev/null + usr/lib/x86_64-linux-gnu/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null + + if [ $stopContainer ]; then + printf "${BLUE}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" + stop_nginx + fi } build_test_runner() { From bb9534e013e7811f320e3b3bbfce7864710f8bfb Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 24 Apr 2023 08:35:08 -0400 Subject: [PATCH 080/130] update README --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3c7d0a7..f8cae3a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. It supports additional features such as extracting claims from the JWT and placing them on the request/response headers. +## Breaking Changes with v2 + +The `v2` branch, which has now been merged to `master` includes breaking changes. Please see the initial v2 release for details, + ## Dependencies This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt). Transitively, that library depends on a JSON Parser called [Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. @@ -160,19 +164,19 @@ Note the `includePath` additions above -- please update them as appropriate. Nex 3. Update the `includePath` entires shown above to match the location you chose. 4. Enter the directory where you extracted NGINX and run: `./configure --with-compat` -#### Cloning libjwt +#### Cloning `libjwt` 1. Clone this repository as follows (replace ``): `git clone git@github.com:benmcollins/libjwt.git 2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` 3. Update the `includePath` entires shown above to match the location you chose. -#### Cloning libjansson +#### Cloning `libjansson` 1. Clone this repository as follows (replace ``): `git clone git@github.com:akheron/jansson.git 2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` 3. Update the `includePath` entires shown above to match the location you chose. -#### Verifing Compliation +#### Verifying Compliation Once you save your changes to `.vscode/c_cpp_properties.json`, you should see that warnings and errors in the Problems panel go away, at least temprorarily. Hopfeully they don't come back, but if they do, make sure your include paths are set correctly. From ab7407180bb6188a4666e2101372c36bd781bc30 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 24 Apr 2023 14:04:27 -0400 Subject: [PATCH 081/130] use next available port for testing --- scripts.sh | 25 +++++++++++++++++++------ test/Dockerfile-test-nginx | 3 +++ test/Dockerfile-test-runner | 5 ++++- test/etc/nginx/conf.d/test.conf | 2 +- test/test.sh | 6 ++++-- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/scripts.sh b/scripts.sh index e5a2380..bbb6fa8 100755 --- a/scripts.sh +++ b/scripts.sh @@ -47,8 +47,10 @@ clean_module() { } start_nginx() { - printf "${BLUE}Starting NGINX container (${IMAGE_NAME})...${NC}\n" - docker run --rm --name "${IMAGE_NAME}" -d -p 8000:80 ${FULL_IMAGE_NAME} >/dev/null + local port=$(get_port) + + printf "${BLUE}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" + docker run --rm --name "${IMAGE_NAME}" -d -p ${PORT}:80 ${FULL_IMAGE_NAME} >/dev/null } stop_nginx() { @@ -82,11 +84,13 @@ build_test_runner() { local dockerArgs=${1:-} local configHash=$(get_hash $(find test -type f -not -name 'test.sh' -not -name '*.yml' -not -name 'Dockerfile*')) local sourceHash=$(get_hash test/test.sh) - - printf "${BLUE}Building test runner...${NC}\n" + local port=$(get_port) + + printf "${BLUE}Building test runner using port ${port}...${NC}\n" docker compose -f ./test/docker-compose-test.yml build ${dockerArgs} \ --build-arg CONFIG_HASH=${configHash}\ - --build-arg SOURCE_HASH=${sourceHash} + --build-arg SOURCE_HASH=${sourceHash} \ + --build-arg PORT=${port} } rebuild_test_runner() { @@ -105,7 +109,7 @@ test() { docker logs ${CONTAINER_NAME_PREFIX} printf "${NC}\n" else - docker start -a ${CONTAINER_NAME_PREFIX}-runner + test_now fi docker compose -f ./test/docker-compose-test.yml down @@ -119,6 +123,15 @@ get_hash() { sha1sum $@ | sed -E 's|\s+|:|' | tr '\n' ' ' | sha1sum | head -c 40 } +get_port() { + for p in $(seq 8000 8100); do + if ! ss -ln | grep -q ":${p} "; then + echo ${p} + break + fi + done +} + if [ $# -eq 0 ]; then all else diff --git a/test/Dockerfile-test-nginx b/test/Dockerfile-test-nginx index 5497d07..b70ca9e 100644 --- a/test/Dockerfile-test-nginx +++ b/test/Dockerfile-test-nginx @@ -1,9 +1,12 @@ ARG BASE_IMAGE ARG CONFIG_HASH +ARG PORT FROM ${BASE_IMAGE} as NGINX ARG CONFIG_HASH +ARG PORT RUN echo "Config Hash: ${CONFIG_HASH}" COPY /docker-entrypoint.d/* /docker-entrypoint.d/ COPY /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.conf COPY /etc/nginx/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf +RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf diff --git a/test/Dockerfile-test-runner b/test/Dockerfile-test-runner index 0992d75..c8cbff2 100644 --- a/test/Dockerfile-test-runner +++ b/test/Dockerfile-test-runner @@ -1,10 +1,13 @@ ARG SOURCE_HASH +ARG PORT FROM alpine:3.7 AS test-base RUN apk add curl bash FROM test-base AS test ARG SOURCE_HASH +ARG PORT +ENV PORT=${PORT} RUN echo "Source Hash: ${SOURCE_HASH}" COPY test.sh . -CMD ["./test.sh"] +CMD ./test.sh ${PORT} diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index fcc3900..62624df 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -2,7 +2,7 @@ error_log /var/log/nginx/debug.log debug; access_log /var/log/nginx/access.log; server { - listen 8000; + listen %{PORT}; server_name localhost; auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; diff --git a/test/test.sh b/test/test.sh index 6ab567b..9516087 100755 --- a/test/test.sh +++ b/test/test.sh @@ -1,6 +1,7 @@ -#!/bin/bash +#!/bin/bash -u # set a test # here to execute only that test and output additional info +PORT=${1:-8000} DEBUG= RED='\e[31m' @@ -45,7 +46,7 @@ run_test () { esac done - curlCommand="curl -s -v http://nginx:8000${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" + curlCommand="curl -s -v http://nginx:${PORT}${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" response=$(eval "${curlCommand}") exitCode=$? @@ -224,4 +225,5 @@ if [ "${DEBUG}" != '' ]; then printf "\n${RED}Some tests will be skipped since DEBUG is set.${NC}\n" fi +printf "\n${GRAY}Starting tests using port ${PORT}...${NC}\n" main From 697551d9b24508ae22aaa8f327a073e1722f2b31 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 24 Apr 2023 14:54:35 -0400 Subject: [PATCH 082/130] fix port casing --- scripts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts.sh b/scripts.sh index bbb6fa8..00a81c7 100755 --- a/scripts.sh +++ b/scripts.sh @@ -50,7 +50,7 @@ start_nginx() { local port=$(get_port) printf "${BLUE}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" - docker run --rm --name "${IMAGE_NAME}" -d -p ${PORT}:80 ${FULL_IMAGE_NAME} >/dev/null + docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME} >/dev/null } stop_nginx() { From b888c93389a51cbe6c13c9c627d5ab2325028a47 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 24 Apr 2023 15:47:31 -0400 Subject: [PATCH 083/130] update to support NGINX 1.23.0+ (#89) --- Dockerfile | 14 +++++++++++++- src/ngx_http_auth_jwt_module.c | 17 ++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 94d829e..7b85a87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,8 @@ RUN <<` apt-get update apt-get install -y curl build-essential ` + + FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module LABEL stage=ngx_http_auth_jwt_builder ENV LD_LIBRARY_PATH=/usr/local/lib @@ -29,7 +31,17 @@ tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx ` WORKDIR /root/build/nginx RUN <<` -./configure --with-debug --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module +BUILD_FLAGS='' +MAJ=$(echo ${NGINX_VERSION} | cut -f1 -d.) +MIN=$(echo ${NGINX_VERSION} | cut -f2 -d.) +REV=$(echo ${NGINX_VERSION} | cut -f3 -d.) + +# NGINX 1.23.0+ changes `cookies` to `cookie` +if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then + BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" +fi + +./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} make modules ` diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 3ecb0d3..ef30437 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -19,6 +19,7 @@ #include "ngx_http_auth_jwt_string.h" #include +#include typedef struct { @@ -639,15 +640,25 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t validation_type) } else if (validation_type.len > sizeof("COOKIE=") && ngx_strncmp(validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1) == 0) { - ngx_int_t n; + bool has_cookie = false; ngx_str_t jwtCookieVal; validation_type.data += sizeof("COOKIE=") - 1; validation_type.len -= sizeof("COOKIE=") - 1; - n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &validation_type, &jwtCookieVal); +#ifndef NGX_LINKED_LIST_COOKIES + if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &validation_type, &jwtCookieVal) != NGX_DECLINED) + { + has_cookie = true; + } +#else + if (ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, &validation_type, &jwtCookieVal) != NULL) + { + has_cookie = true; + } +#endif - if (n != NGX_DECLINED) + if (has_cookie == true) { jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); } From b2ec2bb02fbf654aa1879ab0a7d3e5da5daade40 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 25 Apr 2023 14:02:47 -0400 Subject: [PATCH 084/130] rename `auth_jwt_authorization_type` to `auth_jwt_location` and support pulling JWT from any header (#90) --- Dockerfile | 2 +- README.md | 7 ++-- src/ngx_http_auth_jwt_module.c | 59 ++++++++++++++++----------------- test/etc/nginx/conf.d/test.conf | 46 +++++++++++++++---------- test/test.sh | 23 ++++++++++--- 5 files changed, 81 insertions(+), 56 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7b85a87..4f2db13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ MAJ=$(echo ${NGINX_VERSION} | cut -f1 -d.) MIN=$(echo ${NGINX_VERSION} | cut -f2 -d.) REV=$(echo ${NGINX_VERSION} | cut -f3 -d.) -# NGINX 1.23.0+ changes `cookies` to `cookie` +# NGINX 1.23.0+ changes cookies to use a linked list, and renames `cookies` to `cookie` if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" fi diff --git a/README.md b/README.md index f8cae3a..6a629d3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This module requires several new `nginx.conf` directives, which can be specified | `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | | `auth_jwt_enabled` | Set to "on" to enable JWT checking. | | `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | -| `auth_jwt_validation_type` | Indicates where the JWT is located in the request -- see below. | +| `auth_jwt_location` | Indicates where the JWT is located in the request -- see below. | | `auth_jwt_validate_sub` | Set to "on" to validate the `sub` claim (e.g. user id) in the JWT. | | `auth_jwt_extract_request_claims` | Set to a space-delimited list of claims to extract from the JWT and set as request headers. These will be accessible via e.g: `$http_jwt_sub` | | `auth_jwt_extract_response_claims` | Set to a space-delimited list of claims to extract from the JWT and set as response headers. These will be accessible via e.g: `$sent_http_jwt_sub` | @@ -67,10 +67,11 @@ auth_jwt_redirect off; ``` ## JWT Locations -By default, the authorization header is used to provide a JWT for validation. However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT: +By default, the`Authorization` header is used to provide a JWT for validation. However, you may use the `auth_jwt_location` directive to specify the name of the header or cookie which provides the JWT: ```nginx -auth_jwt_validation_type COOKIE=jwt; +auth_jwt_location HEADER=auth-token; # get the JWT from the "auth-token" header +auth_jwt_location COOKIE=auth-token; # get the JWT from the "auth-token" cookie ``` ## `sub` Validation diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index ef30437..0268dab 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -27,7 +27,7 @@ typedef struct ngx_str_t key; ngx_flag_t enabled; ngx_flag_t redirect; - ngx_str_t validation_type; + ngx_str_t jwt_location; ngx_str_t algorithm; ngx_flag_t validate_sub; ngx_array_t *extract_request_claims; @@ -51,9 +51,9 @@ static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtc static ngx_int_t free_jwt_and_redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf); static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf); -static char *get_jwt(ngx_http_request_t *r, ngx_str_t validation_type); +static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location); -static char *JWT_HEADER_PREFIX = "JWT-"; +static const char *JWT_HEADER_PREFIX = "JWT-"; static ngx_command_t auth_jwt_directives[] = { {ngx_string("auth_jwt_loginurl"), @@ -84,11 +84,11 @@ static ngx_command_t auth_jwt_directives[] = { offsetof(auth_jwt_conf_t, redirect), NULL}, - {ngx_string("auth_jwt_validation_type"), + {ngx_string("auth_jwt_location"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, - offsetof(auth_jwt_conf_t, validation_type), + offsetof(auth_jwt_conf_t, jwt_location), NULL}, {ngx_string("auth_jwt_algorithm"), @@ -208,7 +208,7 @@ static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->loginurl, prev->loginurl, ""); ngx_conf_merge_str_value(conf->key, prev->key, ""); - ngx_conf_merge_str_value(conf->validation_type, prev->validation_type, ""); + ngx_conf_merge_str_value(conf->jwt_location, prev->jwt_location, "HEADER=Authorization"); ngx_conf_merge_str_value(conf->algorithm, prev->algorithm, "HS256"); ngx_conf_merge_str_value(conf->keyfile_path, prev->keyfile_path, ""); ngx_conf_merge_off_value(conf->validate_sub, prev->validate_sub, 0); @@ -311,7 +311,7 @@ static ngx_int_t handle_request(ngx_http_request_t *r) } else { - char *jwtPtr = get_jwt(r, jwtcf->validation_type); + char *jwtPtr = get_jwt(r, jwtcf->jwt_location); if (jwtPtr == NULL) { @@ -608,51 +608,50 @@ static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf) } } -static char *get_jwt(ngx_http_request_t *r, ngx_str_t validation_type) +static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) { + static const char *HEADER_PREFIX = "HEADER="; + static const char *BEARER_PREFIX = "Bearer "; + static const char *COOKIE_PREFIX = "COOKIE="; char *jwtPtr = NULL; - ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "validation_type.len %d", validation_type.len); + ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "jwt_location.len %d", jwt_location.len); - if (validation_type.len == 0 || (validation_type.len == sizeof("AUTHORIZATION") - 1 && ngx_strncmp(validation_type.data, "AUTHORIZATION", sizeof("AUTHORIZATION") - 1) == 0)) + if (jwt_location.len > sizeof(HEADER_PREFIX) && ngx_strncmp(jwt_location.data, HEADER_PREFIX, sizeof(HEADER_PREFIX) - 1) == 0) { - static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); - const ngx_table_elt_t *authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); + ngx_table_elt_t *jwtHeaderVal; - if (authorizationHeader != NULL) - { - ngx_int_t bearer_length = authorizationHeader->value.len - (sizeof("Bearer ") - 1); + jwt_location.data += sizeof(HEADER_PREFIX) - 1; + jwt_location.len -= sizeof(HEADER_PREFIX) - 1; - ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); + jwtHeaderVal = search_headers_in(r, jwt_location.data, jwt_location.len); - if (bearer_length > 0) + if (jwtHeaderVal != NULL) + { + if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, sizeof(BEARER_PREFIX) - 1) == 0) { - ngx_str_t authorizationHeaderStr; - - authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; - authorizationHeaderStr.len = bearer_length; - - jwtPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); - - ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtPtr); + jwtHeaderVal->value.data += sizeof(BEARER_PREFIX) - 1; + jwtHeaderVal->value.len -= sizeof(BEARER_PREFIX) - 1; } + + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderVal->value); } } - else if (validation_type.len > sizeof("COOKIE=") && ngx_strncmp(validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1) == 0) + else if (jwt_location.len > sizeof(COOKIE_PREFIX) && ngx_strncmp(jwt_location.data, COOKIE_PREFIX, sizeof(COOKIE_PREFIX) - 1) == 0) { bool has_cookie = false; ngx_str_t jwtCookieVal; - validation_type.data += sizeof("COOKIE=") - 1; - validation_type.len -= sizeof("COOKIE=") - 1; + jwt_location.data += sizeof(COOKIE_PREFIX) - 1; + jwt_location.len -= sizeof(COOKIE_PREFIX) - 1; #ifndef NGX_LINKED_LIST_COOKIES - if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &validation_type, &jwtCookieVal) != NGX_DECLINED) + if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &jwt_location, &jwtCookieVal) != NGX_DECLINED) { has_cookie = true; } #else - if (ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, &validation_type, &jwtCookieVal) != NULL) + if (ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, &jwt_location, &jwtCookieVal) != NULL) { has_cookie = true; } diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 62624df..3d84e19 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -17,7 +17,7 @@ server { location /secure/cookie/default { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type COOKIE=jwt; + auth_jwt_location COOKIE=jwt; alias /usr/share/nginx/html/; try_files index.html =404; @@ -27,7 +27,7 @@ server { auth_jwt_enabled on; auth_jwt_redirect on; auth_jwt_validate_sub on; - auth_jwt_validation_type COOKIE=jwt; + auth_jwt_location COOKIE=jwt; alias /usr/share/nginx/html/; try_files index.html =404; @@ -36,7 +36,7 @@ server { location /secure/cookie/default/no-redirect { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type COOKIE=jwt; + auth_jwt_location COOKIE=jwt; alias /usr/share/nginx/html/; try_files index.html =404; @@ -45,7 +45,7 @@ server { location /secure/cookie/hs256 { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type COOKIE=jwt; + auth_jwt_location COOKIE=jwt; auth_jwt_algorithm HS256; alias /usr/share/nginx/html/; @@ -55,7 +55,7 @@ server { location /secure/cookie/hs384 { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type COOKIE=jwt; + auth_jwt_location COOKIE=jwt; auth_jwt_algorithm HS384; alias /usr/share/nginx/html/; @@ -65,7 +65,7 @@ server { location /secure/cookie/hs512 { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type COOKIE=jwt; + auth_jwt_location COOKIE=jwt; auth_jwt_algorithm HS512; alias /usr/share/nginx/html/; @@ -75,7 +75,7 @@ server { location /secure/auth-header/default { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; alias /usr/share/nginx/html/; try_files index.html =404; @@ -84,7 +84,7 @@ server { location /secure/auth-header/default/no-redirect { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; alias /usr/share/nginx/html/; try_files index.html =404; @@ -93,7 +93,7 @@ server { location /secure/auth-header/rs256 { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_key "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ @@ -111,7 +111,7 @@ BwIDAQAB location /secure/auth-header/rs256/file { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS256; auth_jwt_use_keyfile on; auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; @@ -123,7 +123,7 @@ BwIDAQAB location /secure/auth-header/rs384/file { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS384; auth_jwt_use_keyfile on; auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; @@ -135,7 +135,7 @@ BwIDAQAB location /secure/auth-header/rs512/file { auth_jwt_enabled on; auth_jwt_redirect on; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS512; auth_jwt_use_keyfile on; auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; @@ -144,10 +144,20 @@ BwIDAQAB try_files index.html =404; } + location /secure/custom-header/hs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Auth-Token; + auth_jwt_algorithm HS256; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + location /secure/extract-claim/request/sub { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_extract_request_claims sub; add_header "Test" "sub=$http_jwt_sub"; @@ -159,7 +169,7 @@ BwIDAQAB location /secure/extract-claim/request/name-1 { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_extract_request_claims firstName lastName; add_header "Test" "$http_jwt_firstname $http_jwt_lastname"; @@ -171,7 +181,7 @@ BwIDAQAB location /secure/extract-claim/request/name-2 { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_extract_request_claims firstName; auth_jwt_extract_request_claims lastName; @@ -184,7 +194,7 @@ BwIDAQAB location /secure/extract-claim/response/sub { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_extract_response_claims sub; add_header "Test" "sub=$sent_http_jwt_sub"; @@ -196,7 +206,7 @@ BwIDAQAB location /secure/extract-claim/response/name-1 { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_extract_response_claims firstName lastName; add_header "Test" "$sent_http_jwt_firstname $sent_http_jwt_lastname"; @@ -208,7 +218,7 @@ BwIDAQAB location /secure/extract-claim/response/name-2 { auth_jwt_enabled on; auth_jwt_redirect off; - auth_jwt_validation_type AUTHORIZATION; + auth_jwt_location HEADER=Authorization; auth_jwt_extract_response_claims firstName; auth_jwt_extract_response_claims lastName; diff --git a/test/test.sh b/test/test.sh index 9516087..af03de5 100755 --- a/test/test.sh +++ b/test/test.sh @@ -109,10 +109,15 @@ main() { -p '/secure/auth-header/default' \ -c '302' - run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 401' \ + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 200' \ -p '/secure/auth-header/default/no-redirect' \ - -c '401' \ - -x '--header "Authorization: X"' + -c '200' \ + -x "--header \"Authorization: ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header with Bearer, should return 200' \ + -p '/secure/auth-header/default/no-redirect' \ + -c '200' \ + -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ -p '/secure/cookie/default' \ @@ -143,7 +148,7 @@ main() { -x ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' run_test -n 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/hs256/' \ + -p '/secure/cookie/hs256' \ -c '200' \ -x '--cookie "jwt=${JWT_HS256_VALID}"' @@ -182,6 +187,16 @@ main() { -c '200' \ -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header without bearer, returns 200' \ + -p '/secure/custom-header/hs256/' \ + -c '200' \ + -x '--header "Auth-Token: ${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header with bearer, returns 200' \ + -p '/secure/custom-header/hs256/' \ + -c '200' \ + -x '--header "Auth-Token: Bearer ${JWT_HS256_VALID}"' + run_test -n 'extracts single claim to request header' \ -p '/secure/extract-claim/request/sub' \ -r '^Test: sub=some-long-uuid$' \ From da1c7ce1c897bd5398b6aa4e0c30b02dc015209e Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 25 Apr 2023 15:01:11 -0400 Subject: [PATCH 085/130] update scripts.sh to add release-related functions --- .gitignore | 1 + scripts.sh | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.gitignore b/.gitignore index fe4f0be..14c2591 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea .vscode bin +release \ No newline at end of file diff --git a/scripts.sh b/scripts.sh index 00a81c7..66b8624 100755 --- a/scripts.sh +++ b/scripts.sh @@ -80,6 +80,31 @@ cp_bin() { fi } +make_release() { + printf "${BLUE}Making release for version ${NGINX_VERSION}...${NC}\n" + + build_module + cp_bin + + mkdir -p release + tar -czvf release/ngx_http_auth_jwt_module_${NGINX_VERSION}.tgz \ + README.md \ + -C bin/usr/lib64/nginx/modules ngx_http_auth_jwt_module.so > /dev/null +} + +# Create releases for the current mainline and stable version, as well as the 2 most recent "legacy" versions. +# See: https://nginx.org/en/download.html +make_releases() { + VERSIONS=(1.20.2 1.22.1 1.24.0 1.23.4) + + rm -rf release/* + + for v in ${VERSIONS[@]}; do + NGINX_VERSION=${v} make_release + done +} + + build_test_runner() { local dockerArgs=${1:-} local configHash=$(get_hash $(find test -type f -not -name 'test.sh' -not -name '*.yml' -not -name 'Dockerfile*')) From d7a369188b1f8ebcf9ee275f5a716b655a17a2bc Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 25 Apr 2023 15:01:24 -0400 Subject: [PATCH 086/130] update default NGINX_VERSION to 1.24.0 (stable) --- scripts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts.sh b/scripts.sh index 66b8624..e115b27 100755 --- a/scripts.sh +++ b/scripts.sh @@ -9,7 +9,7 @@ export ORG_NAME=${ORG_NAME:-teslagov} export IMAGE_NAME=${IMAGE_NAME:-jwt-nginx} export FULL_IMAGE_NAME=${ORG_NAME}/${IMAGE_NAME} export CONTAINER_NAME_PREFIX=${CONTAINER_NAME_PREFIX:-jwt-nginx-test} -export NGINX_VERSION=${NGINX_VERSION:-1.22.0} +export NGINX_VERSION=${NGINX_VERSION:-1.24.0} all() { build_module From 08edb040f96142a72454f02fcaea4411b8c84698 Mon Sep 17 00:00:00 2001 From: "Lewis M. Kabui" <13940255+lewisemm@users.noreply.github.com> Date: Wed, 26 Apr 2023 14:37:53 +0300 Subject: [PATCH 087/130] Add missing backticks to fix markdown format (#92) Co-authored-by: Lewis M. Kabui --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a629d3..d385f23 100644 --- a/README.md +++ b/README.md @@ -167,13 +167,13 @@ Note the `includePath` additions above -- please update them as appropriate. Nex #### Cloning `libjwt` -1. Clone this repository as follows (replace ``): `git clone git@github.com:benmcollins/libjwt.git +1. Clone this repository as follows (replace ``): `git clone git@github.com:benmcollins/libjwt.git ` 2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` 3. Update the `includePath` entires shown above to match the location you chose. #### Cloning `libjansson` -1. Clone this repository as follows (replace ``): `git clone git@github.com:akheron/jansson.git +1. Clone this repository as follows (replace ``): `git clone git@github.com:akheron/jansson.git ` 2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` 3. Update the `includePath` entires shown above to match the location you chose. From 89346e183711197eacbd7c885162176ef563b88d Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 26 Apr 2023 10:46:42 -0400 Subject: [PATCH 088/130] fix extraction of claims in nested config block (#91) --- config | 2 +- src/arrays.c | 14 ++++++++ src/arrays.h | 7 ++++ src/ngx_http_auth_jwt_module.c | 5 +-- test/etc/nginx/conf.d/test.conf | 36 +++++++++++++++++--- test/test.sh | 60 ++++++++++++++++++++++++--------- 6 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 src/arrays.c create mode 100644 src/arrays.h diff --git a/config b/config index 13c2325..0271a79 100644 --- a/config +++ b/config @@ -1,7 +1,7 @@ ngx_module_type=HTTP ngx_addon_name=ngx_http_auth_jwt_module ngx_module_name=$ngx_addon_name -ngx_module_srcs="${ngx_addon_dir}/src/ngx_http_auth_jwt_binary_converters.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_header_processing.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_string.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_module.c" +ngx_module_srcs="${ngx_addon_dir}/src/arrays.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_binary_converters.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_header_processing.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_string.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_module.c" ngx_module_libs="-ljansson -ljwt -lm" . auto/module diff --git a/src/arrays.c b/src/arrays.c new file mode 100644 index 0000000..043c24e --- /dev/null +++ b/src/arrays.c @@ -0,0 +1,14 @@ +#include "arrays.h" +#include + +void merge_array(ngx_pool_t *pool, ngx_array_t **dest, const ngx_array_t *src, size_t size) +{ + // only merge if dest is non-null and src is null + if (src != NULL && *dest == NULL) + { + *dest = ngx_array_create(pool, src->nelts, size); + + ngx_memcpy((*dest)->elts, src->elts, src->nelts * size); + (*dest)->nelts = src->nelts; + } +} diff --git a/src/arrays.h b/src/arrays.h new file mode 100644 index 0000000..5e17158 --- /dev/null +++ b/src/arrays.h @@ -0,0 +1,7 @@ +#ifndef _ARRAYS_H +#define _ARRAYS_H +#include + +void merge_array(ngx_pool_t *pool, ngx_array_t **dest, const ngx_array_t *src, size_t size); + +#endif \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 0268dab..ffc8964 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -14,6 +14,7 @@ #include +#include "arrays.h" #include "ngx_http_auth_jwt_header_processing.h" #include "ngx_http_auth_jwt_binary_converters.h" #include "ngx_http_auth_jwt_string.h" @@ -212,8 +213,8 @@ static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->algorithm, prev->algorithm, "HS256"); ngx_conf_merge_str_value(conf->keyfile_path, prev->keyfile_path, ""); ngx_conf_merge_off_value(conf->validate_sub, prev->validate_sub, 0); - ngx_conf_merge_ptr_value(conf->extract_request_claims, prev->extract_request_claims, NULL); - ngx_conf_merge_ptr_value(conf->extract_request_claims, prev->extract_response_claims, NULL); + merge_array(cf->pool, &conf->extract_request_claims, prev->extract_request_claims, sizeof(ngx_str_t)); + merge_array(cf->pool, &conf->extract_response_claims, prev->extract_response_claims, sizeof(ngx_str_t)); if (conf->enabled == NGX_CONF_UNSET) { diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 3d84e19..71cbd55 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -172,7 +172,7 @@ BwIDAQAB auth_jwt_location HEADER=Authorization; auth_jwt_extract_request_claims firstName lastName; - add_header "Test" "$http_jwt_firstname $http_jwt_lastname"; + add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -185,12 +185,26 @@ BwIDAQAB auth_jwt_extract_request_claims firstName; auth_jwt_extract_request_claims lastName; - add_header "Test" "$http_jwt_firstname $http_jwt_lastname"; + add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; alias /usr/share/nginx/html/; try_files index.html =404; } + location /secure/extract-claim/request/nested { + location /secure/extract-claim/request/nested { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims username; + + add_header "Test" "username=$http_jwt_username"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + } + location /secure/extract-claim/response/sub { auth_jwt_enabled on; auth_jwt_redirect off; @@ -209,7 +223,7 @@ BwIDAQAB auth_jwt_location HEADER=Authorization; auth_jwt_extract_response_claims firstName lastName; - add_header "Test" "$sent_http_jwt_firstname $sent_http_jwt_lastname"; + add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -222,10 +236,24 @@ BwIDAQAB auth_jwt_extract_response_claims firstName; auth_jwt_extract_response_claims lastName; - add_header "Test" "$sent_http_jwt_firstname $sent_http_jwt_lastname"; + add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; alias /usr/share/nginx/html/; try_files index.html =404; } + + location /secure/extract-claim/response/nested { + location /secure/extract-claim/response/nested { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims username; + + add_header "Test" "username=$sent_http_jwt_username"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + } } diff --git a/test/test.sh b/test/test.sh index af03de5..471f658 100755 --- a/test/test.sh +++ b/test/test.sh @@ -56,7 +56,7 @@ run_test () { printf "${RED}${name} -- unexpected exit code from cURL\n\tcURL Exit Code: ${exitCode}"; NUM_FAILED=$((${NUM_FAILED} + 1)); else - OKAY=1 + local okay=1 if [ "${expectedCode}" != "" ]; then local responseCode=$(echo "${response}" | grep -Eo 'HTTP/1.1 ([0-9]{3})' | awk '{print $2}') @@ -64,17 +64,17 @@ run_test () { if [ "${expectedCode}" != "${responseCode}" ]; then printf "${RED}${name} -- unexpected status code\n\tExpected: ${expectedCode}\n\tActual: ${responseCode}\n\tPath: ${path}" NUM_FAILED=$((${NUM_FAILED} + 1)) - OKAY=0 + okay=0 fi fi - - if [ "${OKAY}" == "1" ] && [ "${expectedResponseRegex}" != "" ] && echo "${response}" | grep -Eq "${expectedResponseRegex}"; then + + if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ "${expectedResponseRegex}" ]]; then printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex}" NUM_FAILED=$((${NUM_FAILED} + 1)) - OKAY=0 + okay=0 fi - if [ "${OKAY}" == "1" ]; then + if [ "${okay}" == '1' ]; then printf "${GREEN}${name}"; fi fi @@ -197,34 +197,64 @@ main() { -c '200' \ -x '--header "Auth-Token: Bearer ${JWT_HS256_VALID}"' - run_test -n 'extracts single claim to request header' \ + run_test -n 'extracts single claim to request variable' \ -p '/secure/extract-claim/request/sub' \ - -r '^Test: sub=some-long-uuid$' \ + -r '< Test: sub=some-long-uuid' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - run_test -n 'extracts multiple claims (single directive) to request header' \ + run_test -n 'extracts multiple claims (single directive) to request variable' \ -p '/secure/extract-claim/request/name-1' \ - -r '^Test: hello world$' \ + -r '< Test: firstName=hello; lastName=world' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - run_test -n 'extracts multiple claims (multiple directives) to request header' \ + run_test -n 'extracts multiple claims (multiple directives) to request variable' \ -p '/secure/extract-claim/request/name-2' \ - -r '^Test: hello world$' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to request variable' \ + -p '/secure/extract-claim/request/nested' \ + -r '< Test: username=hello.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to response variable' \ + -p '/secure/extract-claim/response/sub' \ + -r '< Test: sub=some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to response variable' \ + -p '/secure/extract-claim/response/name-1' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to response variable' \ + -p '/secure/extract-claim/response/name-2' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to response variable' \ + -p '/secure/extract-claim/response/nested' \ + -r '< Test: username=hello.world' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' run_test -n 'extracts single claim to response header' \ -p '/secure/extract-claim/response/sub' \ - -r '^Test: sub=some-long-uuid$' \ + -r '< JWT-sub: some-long-uuid' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' run_test -n 'extracts multiple claims (single directive) to response header' \ -p '/secure/extract-claim/response/name-1' \ - -r '^Test: hello world$' \ + -r '< JWT-firstName: hello' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' run_test -n 'extracts multiple claims (multiple directives) to response header' \ -p '/secure/extract-claim/response/name-2' \ - -r '^Test: hello world$' \ + -r '< JWT-firstName: hello' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to response header' \ + -p '/secure/extract-claim/response/nested' \ + -r '< JWT-username: hello.world' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' if [[ "${NUM_FAILED}" = '0' ]]; then From f79e6603ae0ceefa545cb0eb80e5c1d05d96c77a Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 26 Apr 2023 11:11:21 -0400 Subject: [PATCH 089/130] update make_release to include module version number --- scripts.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts.sh b/scripts.sh index e115b27..fa96940 100755 --- a/scripts.sh +++ b/scripts.sh @@ -81,13 +81,16 @@ cp_bin() { } make_release() { - printf "${BLUE}Making release for version ${NGINX_VERSION}...${NC}\n" + local moduleVersion=${1} + local nginxVersion=${2} + + printf "${BLUE}Making release for version ${moduleVersion} for NGINX ${nginxVersion}...${NC}\n" build_module cp_bin mkdir -p release - tar -czvf release/ngx_http_auth_jwt_module_${NGINX_VERSION}.tgz \ + tar -czvf release/ngx_http_auth_jwt_module_${moduleVersion}_nginx_${nginxVersion}.tgz \ README.md \ -C bin/usr/lib64/nginx/modules ngx_http_auth_jwt_module.so > /dev/null } @@ -95,12 +98,13 @@ make_release() { # Create releases for the current mainline and stable version, as well as the 2 most recent "legacy" versions. # See: https://nginx.org/en/download.html make_releases() { - VERSIONS=(1.20.2 1.22.1 1.24.0 1.23.4) + local moduleVersion=$(git describe --tags --abbrev=0) + local nginxVersions=(1.20.2 1.22.1 1.24.0 1.23.4) rm -rf release/* - for v in ${VERSIONS[@]}; do - NGINX_VERSION=${v} make_release + for v in ${nginxVersions[@]}; do + make_release ${moduleVersion} ${v} done } From d4271623eade259f29fc0faab9c6166c32528e0b Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 3 May 2023 08:53:58 -0400 Subject: [PATCH 090/130] add note about string claims --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d385f23..8c34d5c 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ You may specify claims to be extracted from the JWT and placed on the request an If you only wish to access a claim as an NGINX variable, you should use `auth_jwt_extract_request_claims` so that the claim does not end up being sent to the client as a response header. However, if you do want the claim to be sent to the client in the response, then use `auth_jwt_extract_response_claims` instead. +_Please note that `number`, `boolean`, `array`, and `object` claims are not supported at this time -- only `string` claims are supported._ An error will be thrown if you attempt to extract a non-string claim. + ### Using Request Claims For example, you could configure an NGINX location which redirects to the current user's profile. Suppose `sub=abc-123`, the configuration below would redirect to `/profile/abc-123`. From a23ed3c88e71b39ac0eff99086c622f347f0195e Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Thu, 4 May 2023 08:22:20 -0400 Subject: [PATCH 091/130] properly set NGINX_VERSION when making releases (#97) --- scripts.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts.sh b/scripts.sh index fa96940..3922a29 100755 --- a/scripts.sh +++ b/scripts.sh @@ -25,7 +25,7 @@ build_module() { docker image pull debian:bullseye-slim docker image pull nginx:${NGINX_VERSION} - printf "${BLUE}Building module...${NC}\n" + printf "${BLUE}Building module for NGINX ${NGINX_VERSION}...${NC}\n" docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} ${dockerArgs} \ --build-arg NGINX_VERSION=${NGINX_VERSION} \ --build-arg SOURCE_HASH=${sourceHash} . @@ -82,15 +82,16 @@ cp_bin() { make_release() { local moduleVersion=${1} - local nginxVersion=${2} + + NGINX_VERSION=${2} - printf "${BLUE}Making release for version ${moduleVersion} for NGINX ${nginxVersion}...${NC}\n" + printf "${BLUE}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" build_module cp_bin mkdir -p release - tar -czvf release/ngx_http_auth_jwt_module_${moduleVersion}_nginx_${nginxVersion}.tgz \ + tar -czvf release/ngx_http_auth_jwt_module_${moduleVersion}_nginx_${NGINX_VERSION}.tgz \ README.md \ -C bin/usr/lib64/nginx/modules ngx_http_auth_jwt_module.so > /dev/null } From 2eaf11c65029bccde5a0d3da34e0e687dbcb9e8a Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Thu, 10 Aug 2023 12:21:29 -0400 Subject: [PATCH 092/130] fix issue with "Bearer" being removed from header (#106) --- src/ngx_http_auth_jwt_module.c | 17 ++++++++++++----- test/etc/nginx/conf.d/test.conf | 11 +++++++++++ test/test.sh | 6 ++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index ffc8964..744b50d 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -612,7 +612,6 @@ static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf) static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) { static const char *HEADER_PREFIX = "HEADER="; - static const char *BEARER_PREFIX = "Bearer "; static const char *COOKIE_PREFIX = "COOKIE="; char *jwtPtr = NULL; @@ -629,13 +628,21 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) if (jwtHeaderVal != NULL) { + static const char *BEARER_PREFIX = "Bearer "; + if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, sizeof(BEARER_PREFIX) - 1) == 0) { - jwtHeaderVal->value.data += sizeof(BEARER_PREFIX) - 1; - jwtHeaderVal->value.len -= sizeof(BEARER_PREFIX) - 1; - } + ngx_str_t jwtHeaderValWithoutBearer = jwtHeaderVal->value; + + jwtHeaderValWithoutBearer.data += sizeof(BEARER_PREFIX) - 1; + jwtHeaderValWithoutBearer.len -= sizeof(BEARER_PREFIX) - 1; - jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderVal->value); + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderValWithoutBearer); + } + else + { + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderVal->value); + } } } else if (jwt_location.len > sizeof(COOKIE_PREFIX) && ngx_strncmp(jwt_location.data, COOKIE_PREFIX, sizeof(COOKIE_PREFIX) - 1) == 0) diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 71cbd55..00c990b 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -90,6 +90,17 @@ server { try_files index.html =404; } + location /secure/auth-header/default/proxy-header { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + + add_header "Test-Authorization" "$http_authorization"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + location /secure/auth-header/rs256 { auth_jwt_enabled on; auth_jwt_redirect on; diff --git a/test/test.sh b/test/test.sh index 471f658..29a0bf3 100755 --- a/test/test.sh +++ b/test/test.sh @@ -119,6 +119,12 @@ main() { -c '200' \ -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + run_test -n 'when auth enabled with Authorization header with Bearer, should keep header intact' \ + -p '/secure/auth-header/default/proxy-header' \ + -c '200' \ + -r "< Test-Authorization: Bearer ${JWT_HS256_VALID}" \ + -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ -p '/secure/cookie/default' \ -c '302' From 294db83fc613230958dc777edcc28cdde1a8056d Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Thu, 10 Aug 2023 12:42:30 -0400 Subject: [PATCH 093/130] update release versions; test before release --- scripts.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts.sh b/scripts.sh index 3922a29..253abfc 100755 --- a/scripts.sh +++ b/scripts.sh @@ -81,13 +81,17 @@ cp_bin() { } make_release() { + set -e + local moduleVersion=${1} NGINX_VERSION=${2} printf "${BLUE}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" - build_module + rebuild_module + rebuild_test_runner + test cp_bin mkdir -p release @@ -100,7 +104,7 @@ make_release() { # See: https://nginx.org/en/download.html make_releases() { local moduleVersion=$(git describe --tags --abbrev=0) - local nginxVersions=(1.20.2 1.22.1 1.24.0 1.23.4) + local nginxVersions=('1.25.1' '1.24.0' '1.22.1' '1.20.2') rm -rf release/* From 5f9ffd20fec5fc31c90d5851c06747456a608e51 Mon Sep 17 00:00:00 2001 From: KnownEntity <42591012+KnownEntity@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:08:46 -0500 Subject: [PATCH 094/130] GitHub Action to automatically build master branch on commit (#108) Co-authored-by: Josh McCullough --- .github/workflows/ci.yml | 133 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d7f4b6b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,133 @@ +name: CI + +on: + push: + branches: + - 'master' + pull_request: + branches: + - 'master' + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + # Each nginx version to build against + nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.25.1'] + # The following versions of libjwt are compatible: + # * v1.0 - v1.12.0 + # * v1.12.1 - v1.14.0 + # * v1.15.0+ + # At the time of writing this: + # * Debian and Ubuntu's repos have v1.10.2 + # * EPEL has v1.12.1 + # This compilles against each version prior to a breaking change and the latest release + libjwt-version: ['1.12.0', '1.14.0', '1.15.3'] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + path: 'ngx-http-auth-jwt-module' + + - name: Download jansson + uses: actions/checkout@v3 + with: + repository: 'akheron/jansson' + ref: 'v2.14' + path: 'jansson' + + - name: Build jansson + working-directory: ./jansson + run: | + cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ + make && \ + make check && \ + sudo make install + + - name: Download libjwt + uses: actions/checkout@v3 + with: + repository: 'benmcollins/libjwt' + ref: 'v${{matrix.libjwt-version}}' + path: 'libjwt' + + - name: Build libjwt + working-directory: ./libjwt + run: | + autoreconf -i && \ + ./configure && \ + make all && \ + sudo make install + + - name: Download NGINX + run: | + mkdir nginx + curl -O http://nginx.org/download/nginx-${{matrix.nginx-version}}.tar.gz + tar -xzf nginx-${{matrix.nginx-version}}.tar.gz --strip-components 1 -C nginx + + - name: Run configure + working-directory: ./nginx + run: | + BUILD_FLAGS='' + MAJ=$(echo ${{matrix.nginx-version}} | cut -f1 -d.) + MIN=$(echo ${{matrix.nginx-version}} | cut -f2 -d.) + REV=$(echo ${{matrix.nginx-version}} | cut -f3 -d.) + if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then + BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" + fi + ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} + + - name: Run make + working-directory: ./nginx + run: make modules + + - name: Create release archive + run: | + cp ./nginx/objs/ngx_http_auth_jwt_module.so ./ + tar czf ngx_http_auth_jwt_module_${{github.ref_name}}_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz ngx_http_auth_jwt_module.so + + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: ngx_http_auth_jwt_module_${{github.ref_name}}_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz + path: ngx_http_auth_jwt_module_${{github.ref_name}}_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz + + update_releases_page: + name: Upload builds to Releases + if: github.event_name != 'pull_request' + needs: + - build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Set up variables + id: vars + run: | + echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" + + - name: Download build artifacts from previous jobs + uses: actions/download-artifact@v3 + with: + path: artifacts + + - name: Upload builds to Releases + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: artifacts/*/* + body: | + > [!WARNING] + > This is an automatically generated pre-release version of the module, which includes the latest master branch changes. + > Please report any bugs you find to the issue tracker. + + - Build Date: `${{ steps.vars.outputs.date_now }}` + - Commit: ${{ github.sha }} + name: 'Development build: ${{ github.ref_name }}@${{ github.sha }}' + prerelease: true + removeArtifacts: true + tag: dev-build From c843ce15f5b7c162e58c15c5aa01de33bbd8df66 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 25 Aug 2023 08:09:53 -0400 Subject: [PATCH 095/130] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c34d5c..a5b7018 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This module requires several new `nginx.conf` directives, which can be specified ## Algorithms -The default algorithm is `HS256`, for symmetric key validation. When using one of the `HS*` algorithms, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see [NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final), Section 5.3.2 The HMAC Key. +The default algorithm is `HS256`, for symmetric key validation. When using one of the `HS*` algorithms, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total). Note that using more than 512 bits will not increase the security. For key guidelines please see [NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final), Section 5.3.2 The HMAC Key. ### Additional Supported Algorithms From e5d629e30b631ea363a82cd02a01d1a483f641a0 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 25 Aug 2023 08:22:24 -0400 Subject: [PATCH 096/130] update CI to only run if src dir is changed --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7f4b6b..9177330 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,13 @@ on: push: branches: - 'master' + paths: + - src/** pull_request: branches: - 'master' + paths: + - src/** workflow_dispatch: jobs: From 0b8e193c95ed881770545058b76099e87132edba Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 25 Aug 2023 08:23:56 -0400 Subject: [PATCH 097/130] add hex generation to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a5b7018..51c7196 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ This module requires several new `nginx.conf` directives, which can be specified The default algorithm is `HS256`, for symmetric key validation. When using one of the `HS*` algorithms, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total). Note that using more than 512 bits will not increase the security. For key guidelines please see [NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final), Section 5.3.2 The HMAC Key. +To generate a 256-bit key (32 pairs of hex characters; 64 characters in total): + +```bash +openssl rand -hex 32 +``` + ### Additional Supported Algorithms The configuration also supports RSA public key validation via (e.g.) `auth_jwt_algorithm RS256`. When using the `RS*` alhorithms, the `auth_jwt_key` field must be set to your public key **OR** `auth_jwt_use_keyfile` should be set to `on` and `auth_jwt_keyfile_path` should point to the public key on disk. NGINX won't start if `auth_jwt_use_keyfile` is set to `on` and a key file is not provided. From 05a37988674aa3f2a63d80a3d43abb6684408cca Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Fri, 13 Oct 2023 18:51:48 -0400 Subject: [PATCH 098/130] clarify test logging --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51c7196..ba3dde1 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ The tests use a customized NGINX image, distinct from the main image, as well as After making changes and finding that some tests fail, it can be difficult to understand why. By default, logs are written to Docker's internal log mechanism, but they won't be persisted after the test run completes and the containers are removed. -In order to persist logs, you can configure the log driver to use. You can do this by setting the environment variable `LOG_DRIVER` before running the tests. On Linux/Unix systems, you can use the driver `journald`, as follows: +If you'd like to persist logs across test runs, you can configure the log driver to use `journald` (on Linux/Unix systems for example). You can do this by setting the environment variable `LOG_DRIVER` before running the tests: ```shell # need to rebuild the test runner with the proper log driver From 2062be55dd8624ab8f4d06d27fc455bbc51a730b Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 4 Dec 2023 11:21:01 -0500 Subject: [PATCH 099/130] update scripts.sh to clarify target NGINX versions --- scripts.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts.sh b/scripts.sh index 253abfc..175256c 100755 --- a/scripts.sh +++ b/scripts.sh @@ -5,11 +5,17 @@ GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' +# supported NGINX versions -- for binary distribution +NGINX_VERSION_MAINLINE='1.25.3' +NGINX_VERSION_STABLE='1.24.0' +NGINX_VERSION_LEGACY_1='1.22.1' +NGINX_VERSION_LEGACY_2='1.20.2' + export ORG_NAME=${ORG_NAME:-teslagov} export IMAGE_NAME=${IMAGE_NAME:-jwt-nginx} export FULL_IMAGE_NAME=${ORG_NAME}/${IMAGE_NAME} export CONTAINER_NAME_PREFIX=${CONTAINER_NAME_PREFIX:-jwt-nginx-test} -export NGINX_VERSION=${NGINX_VERSION:-1.24.0} +export NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} all() { build_module @@ -104,8 +110,8 @@ make_release() { # See: https://nginx.org/en/download.html make_releases() { local moduleVersion=$(git describe --tags --abbrev=0) - local nginxVersions=('1.25.1' '1.24.0' '1.22.1' '1.20.2') - + local nginxVersions=(${NGINX_VERSION_MAINLINE} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2}) + rm -rf release/* for v in ${nginxVersions[@]}; do From 07f6f996975e6619a5e7a82ee7a42bc8177ef29f Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 4 Dec 2023 13:32:29 -0500 Subject: [PATCH 100/130] replace `sizeof` with `strlen` (#116) --- .github/workflows/ci.yml | 9 +++++---- src/ngx_http_auth_jwt_module.c | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9177330..3c31403 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,11 @@ on: jobs: build: + name: "NGINX: ${{ matrix.nginx-version }}; libjwt: ${{ matrix.libjwt-version }}" strategy: matrix: # Each nginx version to build against - nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.25.1'] + nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.25.3'] # The following versions of libjwt are compatible: # * v1.0 - v1.12.0 # * v1.12.1 - v1.14.0 @@ -90,14 +91,14 @@ jobs: - name: Create release archive run: | cp ./nginx/objs/ngx_http_auth_jwt_module.so ./ - tar czf ngx_http_auth_jwt_module_${{github.ref_name}}_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz ngx_http_auth_jwt_module.so + tar czf ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz ngx_http_auth_jwt_module.so - name: Upload build artifact uses: actions/upload-artifact@v3 with: if-no-files-found: error - name: ngx_http_auth_jwt_module_${{github.ref_name}}_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz - path: ngx_http_auth_jwt_module_${{github.ref_name}}_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz + name: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz + path: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz update_releases_page: name: Upload builds to Releases diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 744b50d..edef14a 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -489,7 +489,7 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) } r->headers_out.location->hash = 1; - r->headers_out.location->key.len = sizeof("Location") - 1; + r->headers_out.location->key.len = strlen("Location"); r->headers_out.location->key.data = (u_char *)"Location"; if (r->method == NGX_HTTP_GET) @@ -526,21 +526,21 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) uri_escaped.len = escaped_len; ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); - r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; + r->headers_out.location->value.len = loginlen + strlen("?return_url=") + strlen(scheme) + strlen("://") + server.len + uri_escaped.len; return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); ngx_memcpy(return_url, jwtcf->loginurl.data, jwtcf->loginurl.len); return_url_idx = jwtcf->loginurl.len; - ngx_memcpy(return_url + return_url_idx, "?return_url=", sizeof("?return_url=") - 1); + ngx_memcpy(return_url + return_url_idx, "?return_url=", strlen("?return_url=")); - return_url_idx += sizeof("?return_url=") - 1; + return_url_idx += strlen("?return_url="); ngx_memcpy(return_url + return_url_idx, scheme, strlen(scheme)); return_url_idx += strlen(scheme); - ngx_memcpy(return_url + return_url_idx, "://", sizeof("://") - 1); + ngx_memcpy(return_url + return_url_idx, "://", strlen("://")); - return_url_idx += sizeof("://") - 1; + return_url_idx += strlen("://"); ngx_memcpy(return_url + return_url_idx, server.data, server.len); return_url_idx += server.len; @@ -617,12 +617,12 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "jwt_location.len %d", jwt_location.len); - if (jwt_location.len > sizeof(HEADER_PREFIX) && ngx_strncmp(jwt_location.data, HEADER_PREFIX, sizeof(HEADER_PREFIX) - 1) == 0) + if (jwt_location.len > strlen(HEADER_PREFIX) && ngx_strncmp(jwt_location.data, HEADER_PREFIX, strlen(HEADER_PREFIX)) == 0) { ngx_table_elt_t *jwtHeaderVal; - jwt_location.data += sizeof(HEADER_PREFIX) - 1; - jwt_location.len -= sizeof(HEADER_PREFIX) - 1; + jwt_location.data += strlen(HEADER_PREFIX); + jwt_location.len -= strlen(HEADER_PREFIX); jwtHeaderVal = search_headers_in(r, jwt_location.data, jwt_location.len); @@ -630,12 +630,12 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) { static const char *BEARER_PREFIX = "Bearer "; - if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, sizeof(BEARER_PREFIX) - 1) == 0) + if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) { ngx_str_t jwtHeaderValWithoutBearer = jwtHeaderVal->value; - jwtHeaderValWithoutBearer.data += sizeof(BEARER_PREFIX) - 1; - jwtHeaderValWithoutBearer.len -= sizeof(BEARER_PREFIX) - 1; + jwtHeaderValWithoutBearer.data += strlen(BEARER_PREFIX); + jwtHeaderValWithoutBearer.len -= strlen(BEARER_PREFIX); jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderValWithoutBearer); } @@ -645,13 +645,13 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) } } } - else if (jwt_location.len > sizeof(COOKIE_PREFIX) && ngx_strncmp(jwt_location.data, COOKIE_PREFIX, sizeof(COOKIE_PREFIX) - 1) == 0) + else if (jwt_location.len > strlen(COOKIE_PREFIX) && ngx_strncmp(jwt_location.data, COOKIE_PREFIX, strlen(COOKIE_PREFIX)) == 0) { bool has_cookie = false; ngx_str_t jwtCookieVal; - jwt_location.data += sizeof(COOKIE_PREFIX) - 1; - jwt_location.len -= sizeof(COOKIE_PREFIX) - 1; + jwt_location.data += strlen(COOKIE_PREFIX); + jwt_location.len -= strlen(COOKIE_PREFIX); #ifndef NGX_LINKED_LIST_COOKIES if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &jwt_location, &jwtCookieVal) != NGX_DECLINED) From 736a95a2dabafa31e470c3938895d514f95585d5 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 20 Feb 2024 19:53:30 -0500 Subject: [PATCH 101/130] update NGINX mainline version --- scripts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts.sh b/scripts.sh index 175256c..e3fb254 100755 --- a/scripts.sh +++ b/scripts.sh @@ -6,7 +6,7 @@ RED='\033[0;31m' NC='\033[0m' # supported NGINX versions -- for binary distribution -NGINX_VERSION_MAINLINE='1.25.3' +NGINX_VERSION_MAINLINE='1.25.4' NGINX_VERSION_STABLE='1.24.0' NGINX_VERSION_LEGACY_1='1.22.1' NGINX_VERSION_LEGACY_2='1.20.2' From 032fa5c14b93d309d88b585f084e4db7dbd2f53c Mon Sep 17 00:00:00 2001 From: Stephan Wurm Date: Mon, 18 Mar 2024 17:32:42 +0100 Subject: [PATCH 102/130] add support for ES algorithms (#118) Signed-off-by: Stephan Wurm --- src/ngx_http_auth_jwt_module.c | 6 +- test/Dockerfile-test-nginx | 3 + test/ec_key_256.pem | 5 ++ test/ec_key_384.pem | 6 ++ test/ec_key_521.pem | 8 ++ test/etc/nginx/conf.d/test.conf | 123 ++++++++++++++++++++++++++++++ test/etc/nginx/ec_key_256-pub.pem | 4 + test/etc/nginx/ec_key_384-pub.pem | 5 ++ test/etc/nginx/ec_key_521-pub.pem | 6 ++ test/test.sh | 39 ++++++++++ 10 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 test/ec_key_256.pem create mode 100644 test/ec_key_384.pem create mode 100644 test/ec_key_521.pem create mode 100644 test/etc/nginx/ec_key_256-pub.pem create mode 100644 test/etc/nginx/ec_key_384-pub.pem create mode 100644 test/etc/nginx/ec_key_521-pub.pem diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index edef14a..85a646d 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -337,7 +337,7 @@ static ngx_int_t handle_request(ngx_http_request_t *r) return redirect(r, jwtcf); } } - else if (algorithm.len == 5 && ngx_strncmp(algorithm.data, "RS", 2) == 0) + else if (algorithm.len == 5 && (ngx_strncmp(algorithm.data, "RS", 2) == 0 || ngx_strncmp(algorithm.data, "ES", 2) == 0)) { if (jwtcf->use_keyfile == 1) { @@ -394,7 +394,7 @@ static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { const jwt_alg_t alg = jwt_get_alg(jwt); - if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512) + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512 && alg != JWT_ALG_ES256 && alg != JWT_ALG_ES384 && alg != JWT_ALG_ES512) { return 1; } @@ -633,7 +633,7 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) { ngx_str_t jwtHeaderValWithoutBearer = jwtHeaderVal->value; - + jwtHeaderValWithoutBearer.data += strlen(BEARER_PREFIX); jwtHeaderValWithoutBearer.len -= strlen(BEARER_PREFIX); diff --git a/test/Dockerfile-test-nginx b/test/Dockerfile-test-nginx index b70ca9e..5f01436 100644 --- a/test/Dockerfile-test-nginx +++ b/test/Dockerfile-test-nginx @@ -9,4 +9,7 @@ RUN echo "Config Hash: ${CONFIG_HASH}" COPY /docker-entrypoint.d/* /docker-entrypoint.d/ COPY /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.conf COPY /etc/nginx/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf +COPY /etc/nginx/ec_key_256-pub.pem /etc/nginx/ec-256-key.conf +COPY /etc/nginx/ec_key_384-pub.pem /etc/nginx/ec-384-key.conf +COPY /etc/nginx/ec_key_521-pub.pem /etc/nginx/ec-521-key.conf RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf diff --git a/test/ec_key_256.pem b/test/ec_key_256.pem new file mode 100644 index 0000000..4206969 --- /dev/null +++ b/test/ec_key_256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOlEBGcZxxhv8FkN0 +YIvax6fnhJbMeotzIEBxIglkNu6hRANCAATP1NpDzvZmKd2Mw6hIrv4nzUfNu7OK +mT5VuL5LhvUgzTqVGuxwevA7DlFsNVSfCljIBG3geio3fcd4k0Z9SygL +-----END PRIVATE KEY----- diff --git a/test/ec_key_384.pem b/test/ec_key_384.pem new file mode 100644 index 0000000..2aa5780 --- /dev/null +++ b/test/ec_key_384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDADyrL6llSQoQOZ/PF/ +l761kAbrTwn4vu30Kr34ScW6bRKVXLq3cT3QssJ1nF9B63qhZANiAAQ48dOfIEd3 +0TCVE0JT4ZU0Db7Ftz+ex7lojP7uqTY9OI59yoMB01zUN4JK30BRXS9Yv0A9Bu1z +fgLu93FSn0kd0zIPMvuu5LUt60M/miSt2lA0OrqFhKjx6FFdN/lNh64= +-----END PRIVATE KEY----- diff --git a/test/ec_key_521.pem b/test/ec_key_521.pem new file mode 100644 index 0000000..10471dc --- /dev/null +++ b/test/ec_key_521.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAKkag6aVn4XAbaALo +0b3pypdP5RBX7uKxHmKlkNCcpA0oVTdgjnM5NpJP8ZOM6NjVhEzsn6c/Tdn8hL8w +SI55hFWhgYkDgYYABABpTipSvbs8fq44u4fA+v7DTNYViA58sqbrxjxdzwWZ8eEj +CXsH7yzSGx3Y19NSyrX8HbjWmrj5uxiKeFCB8mGzTwDcFIKCMeMkHjZs/fmVOumR +a2XSpj7BP6wqcN6Pf+UqECivGAZGRHoabo/dm5zF9M3gO+G9eOrf3G1wgFFM7Vzb +Ow== +-----END PRIVATE KEY----- diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 00c990b..3421b5b 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -72,6 +72,51 @@ server { try_files index.html =404; } + location /secure/cookie/es256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES256; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES384; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES512; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + location /secure/auth-header/default { auth_jwt_enabled on; auth_jwt_redirect on; @@ -119,6 +164,48 @@ BwIDAQAB try_files index.html =404; } + location /secure/auth-header/es256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + location /secure/auth-header/rs256/file { auth_jwt_enabled on; auth_jwt_redirect on; @@ -155,6 +242,42 @@ BwIDAQAB try_files index.html =404; } + location /secure/auth-header/es256/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES256; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec-256-key.conf"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es384/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES384; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec-384-key.conf"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es512/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES512; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec-521-key.conf"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + location /secure/custom-header/hs256 { auth_jwt_enabled on; auth_jwt_redirect on; diff --git a/test/etc/nginx/ec_key_256-pub.pem b/test/etc/nginx/ec_key_256-pub.pem new file mode 100644 index 0000000..3306ea0 --- /dev/null +++ b/test/etc/nginx/ec_key_256-pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_384-pub.pem b/test/etc/nginx/ec_key_384-pub.pem new file mode 100644 index 0000000..e642ed1 --- /dev/null +++ b/test/etc/nginx/ec_key_384-pub.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_521-pub.pem b/test/etc/nginx/ec_key_521-pub.pem new file mode 100644 index 0000000..0cb875c --- /dev/null +++ b/test/etc/nginx/ec_key_521-pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY----- diff --git a/test/test.sh b/test/test.sh index 29a0bf3..5671b8f 100755 --- a/test/test.sh +++ b/test/test.sh @@ -100,6 +100,10 @@ main() { local JWT_RS256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 local JWT_RS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.H35bTcZRhepWIoa8pKCbUMRuAOkVX9K5hJjc6tPmQwWmTw8lrktsvmMzJg_rgqnJLnAkciSIQw5EDj7fngS5zX2ThyRxrkPuE2Uiyw2Ect-mo9Kg1lrWgnyZCuCgq-Up9HQRAv0160mePlm8Gs4TOY6CPr38zwTcDZsy_Keq93igDQV8WuuWAGICaGd5ZyUOPjjzGShRjTU8Szz7fnpZpTtYRCYVo0pc5yfRWYm0fdn-4AseyGvd8JJ2xfnAEe4kZOkz7X1MLKtL0slKg3m2PH1lD7HwxIawXRTPWxArhJ9dcTNiDUrqtde2juGwOuMD_zTsb2Jj0_rmRb0Q6aljNw local JWT_RS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.iUupyKypfXJ5aZWfItSW-mOmx9a4C4X7Yr5p5Fk8W75ZhkOq0EeNfstTxx870brhkdPovBhO2LYI44_HoH9XicQNL6JnFprE0r61eJFngbuzlhRQiWpq0xYrazJWc9zB7_GgL2ZCwtw-Ts3G23Q0632wVm6-d7MKvG7RS8aEjN-MuVGdtLglH3forpItmFxw-if40EQsBL7hncN_XNcQTO4KPHkqmlpac_oKXRrLFDIIt2tB6OOpvY4QcpERoxexp4pi2f-JoINnWX_dU5JnIs3ypVJLQPfoJvxg8fsg3zYrOvMYnfsqOCYoHtZGK0O7jyfFmcGo5v2hLT-CpoF3Zw + local JWT_ES256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.WFfJXGr5whKHB7arjsTXPTJ6TAsS1LoRxu7Vj2_HrLaIQphWJM6BICf-M3cv52tFzt-XTZb6GxlDgAbHo8z9Zg + local JWT_ES256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 + local JWT_ES384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._EFxXYOTAfT3gB3xUfgGR2UyXHeRTlDWqA94oZbB0DDa7YPZTEX9T4C_0ylnOFKZ6irGHZA8vxjgXDH3DZKWwBWcZ-XaQ_Q4Ws2J-AEeLqcl7_CS6q9mFo0Y7vUNEn-W + local JWT_ES512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.AFY4gNCtZNYkrTiijDkV4eKIt2UPMIuJBfZIk69jgI8FSGCQyUIMmIVg0fTvbaSiaryXzcjbG5TCm8a9Vu3KFJutAHGrgvZqcdklxx6Fbk3an3r_CH68n_ncwS3SUV58mDjf0OX8jRuNdudU1L5xYNQdodo-fxPIb1oHXfMJ0CmULDR9 run_test -n 'when auth disabled, should return 200' \ -p '/' \ @@ -173,6 +177,21 @@ main() { -c '200' \ -x ' --cookie "jwt=${JWT_RS256_VALID}"' + run_test -n 'when auth enabled with ES256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es256' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES256_VALID}"' + + run_test -n 'when auth enabled with ES384 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es384' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES384_VALID}"' + + run_test -n 'when auth enabled with ES512 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es512' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES512_VALID}"' + run_test -n 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ -p '/secure/auth-header/rs256/file' \ -c '200' \ @@ -193,6 +212,26 @@ main() { -c '200' \ -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + run_test -n 'when auth enabled with ES256 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es256/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + -p '/secure/auth-header/es256/file' \ + -c '302' \ + -x '--header "Authorization: Bearer ${JWT_ES256_INVALID}"' + + run_test -n 'when auth enabled with ES384 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es384/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES384_VALID}"' + + run_test -n 'when auth enabled with ES512 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es512/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES512_VALID}"' + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header without bearer, returns 200' \ -p '/secure/custom-header/hs256/' \ -c '200' \ From 03d95531d14afea1a2acbd023fd1cd8ae234881a Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 19 Mar 2024 14:32:57 -0400 Subject: [PATCH 103/130] build custom SSL images; add SSL tests (#126) --- .../workflows/{ci.yml => make-releases.yml} | 27 +-- Dockerfile | 59 ------- nginx.dockerfile | 136 +++++++++++++++ openssl.dockerfile | 37 ++++ scripts.sh | 165 ++++++++++++------ test/Dockerfile-test-nginx | 15 -- test/Dockerfile-test-runner | 13 -- test/docker-compose-test.yml | 13 +- test/docker-entrypoint.d/10-nginx-test.sh | 1 - test/etc/nginx/conf.d/test.conf | 19 +- test/etc/nginx/test.crt | 23 +++ test/etc/nginx/test.key | 28 +++ test/test-nginx.dockerfile | 18 ++ test/test-runner.dockerfile | 16 ++ test/test.sh | 64 ++++--- 15 files changed, 442 insertions(+), 192 deletions(-) rename .github/workflows/{ci.yml => make-releases.yml} (89%) delete mode 100644 Dockerfile create mode 100644 nginx.dockerfile create mode 100644 openssl.dockerfile delete mode 100644 test/Dockerfile-test-nginx delete mode 100644 test/Dockerfile-test-runner delete mode 100755 test/docker-entrypoint.d/10-nginx-test.sh create mode 100644 test/etc/nginx/test.crt create mode 100644 test/etc/nginx/test.key create mode 100644 test/test-nginx.dockerfile create mode 100644 test/test-runner.dockerfile diff --git a/.github/workflows/ci.yml b/.github/workflows/make-releases.yml similarity index 89% rename from .github/workflows/ci.yml rename to .github/workflows/make-releases.yml index 3c31403..037823b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/make-releases.yml @@ -3,12 +3,12 @@ name: CI on: push: branches: - - 'master' + - master paths: - src/** pull_request: branches: - - 'master' + - master paths: - src/** workflow_dispatch: @@ -18,8 +18,9 @@ jobs: name: "NGINX: ${{ matrix.nginx-version }}; libjwt: ${{ matrix.libjwt-version }}" strategy: matrix: - # Each nginx version to build against + # NGINX versions to build/test against nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.25.3'] + # The following versions of libjwt are compatible: # * v1.0 - v1.12.0 # * v1.12.1 - v1.14.0 @@ -27,15 +28,16 @@ jobs: # At the time of writing this: # * Debian and Ubuntu's repos have v1.10.2 # * EPEL has v1.12.1 - # This compilles against each version prior to a breaking change and the latest release + # This compiles against each version prior to a breaking change and the latest release libjwt-version: ['1.12.0', '1.14.0', '1.15.3'] runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout Code uses: actions/checkout@v3 with: path: 'ngx-http-auth-jwt-module' + # TODO cache the build result so we don't have to do this every time? - name: Download jansson uses: actions/checkout@v3 with: @@ -50,7 +52,8 @@ jobs: make && \ make check && \ sudo make install - + + # TODO cache the build result so we don't have to do this every time? - name: Download libjwt uses: actions/checkout@v3 with: @@ -71,20 +74,22 @@ jobs: mkdir nginx curl -O http://nginx.org/download/nginx-${{matrix.nginx-version}}.tar.gz tar -xzf nginx-${{matrix.nginx-version}}.tar.gz --strip-components 1 -C nginx - - - name: Run configure + + - name: Configure NGINX working-directory: ./nginx run: | BUILD_FLAGS='' MAJ=$(echo ${{matrix.nginx-version}} | cut -f1 -d.) MIN=$(echo ${{matrix.nginx-version}} | cut -f2 -d.) REV=$(echo ${{matrix.nginx-version}} | cut -f3 -d.) + if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then - BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" + BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" fi - ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} - - name: Run make + ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} + + - name: Make Modules working-directory: ./nginx run: make modules diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4f2db13..0000000 --- a/Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -ARG NGINX_VERSION -ARG SOURCE_HASH - - -FROM debian:bullseye-slim as ngx_http_auth_jwt_builder_base -LABEL stage=ngx_http_auth_jwt_builder -RUN <<` -apt-get update -apt-get install -y curl build-essential -` - - -FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module -LABEL stage=ngx_http_auth_jwt_builder -ENV LD_LIBRARY_PATH=/usr/local/lib -ARG NGINX_VERSION -RUN <<` -apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev -mkdir -p /root/build/ngx-http-auth-jwt-module -` -WORKDIR /root/build/ngx-http-auth-jwt-module -ARG SOURCE_HASH -RUN echo "Source Hash: ${SOURCE_HASH}" -ADD config ./ -ADD src/*.h src/*.c ./src/ -WORKDIR /root/build -RUN <<` -mkdir nginx -curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz -tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx -` -WORKDIR /root/build/nginx -RUN <<` -BUILD_FLAGS='' -MAJ=$(echo ${NGINX_VERSION} | cut -f1 -d.) -MIN=$(echo ${NGINX_VERSION} | cut -f2 -d.) -REV=$(echo ${NGINX_VERSION} | cut -f3 -d.) - -# NGINX 1.23.0+ changes cookies to use a linked list, and renames `cookies` to `cookie` -if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then - BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" -fi - -./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} -make modules -` - - -FROM nginx:${NGINX_VERSION} AS ngx_http_auth_jwt_builder_nginx -LABEL stage= -RUN rm /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh /etc/nginx/conf.d/default.conf -RUN <<` -apt-get update -apt-get -y install libjansson4 libjwt0 -cd /etc/nginx -sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf -` -LABEL maintainer="TeslaGov" email="developers@teslagov.com" -COPY --from=ngx_http_auth_jwt_builder_module /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ diff --git a/nginx.dockerfile b/nginx.dockerfile new file mode 100644 index 0000000..21c7460 --- /dev/null +++ b/nginx.dockerfile @@ -0,0 +1,136 @@ +ARG BASE_IMAGE +ARG NGINX_VERSION + + +FROM ${BASE_IMAGE} as ngx_http_auth_jwt_builder_base +LABEL stage=ngx_http_auth_jwt_builder +RUN <<` +apt-get update +apt-get install -y curl build-essential +` + + +FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module +LABEL stage=ngx_http_auth_jwt_builder +ENV PATH "${PATH}:/etc/nginx" +ENV LD_LIBRARY_PATH=/usr/local/lib +ARG NGINX_VERSION +RUN <<` + set -e + apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev + mkdir -p /root/build/ngx-http-auth-jwt-module +` +WORKDIR /root/build/ngx-http-auth-jwt-module +ADD config ./ +ADD src/*.h src/*.c ./src/ +WORKDIR /root/build +RUN <<` + set -e + mkdir nginx + curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz + tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx +` +WORKDIR /root/build/nginx +RUN <<` + set -e + BUILD_FLAGS='' + MAJ=$(echo ${NGINX_VERSION} | cut -f1 -d.) + MIN=$(echo ${NGINX_VERSION} | cut -f2 -d.) + REV=$(echo ${NGINX_VERSION} | cut -f3 -d.) + + # NGINX 1.23.0+ changes cookies to use a linked list, and renames `cookies` to `cookie` + if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then + BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" + fi + + ./configure \ + --prefix=/etc/nginx \ + --sbin-path=/usr/sbin/nginx \ + --modules-path=/usr/lib64/nginx/modules \ + --conf-path=/etc/nginx/nginx.conf \ + --error-log-path=/var/log/nginx/error.log \ + --http-log-path=/var/log/nginx/access.log \ + --pid-path=/var/run/nginx.pid \ + --lock-path=/var/run/nginx.lock \ + --http-client-body-temp-path=/var/cache/nginx/client_temp \ + --http-proxy-temp-path=/var/cache/nginx/proxy_temp \ + --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ + --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ + --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ + --user=nginx \ + --group=nginx \ + --with-compat \ + --with-debug \ + --with-file-aio \ + --with-threads \ + --with-http_addition_module \ + --with-http_auth_request_module \ + --with-http_dav_module \ + --with-http_flv_module \ + --with-http_gunzip_module \ + --with-http_gzip_static_module \ + --with-http_mp4_module \ + --with-http_random_index_module \ + --with-http_realip_module \ + --with-http_secure_link_module \ + --with-http_slice_module \ + --with-http_ssl_module \ + --with-http_stub_status_module \ + --with-http_sub_module \ + --with-http_v2_module \ + --with-mail \ + --with-mail_ssl_module \ + --with-stream \ + --with-stream_realip_module \ + --with-stream_ssl_module \ + --with-stream_ssl_preread_module \ + --with-cc-opt='-g -O2 -ffile-prefix-map=/data/builder/debuild/nginx-1.25.4/debian/debuild-base/nginx-1.25.4=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' \ + --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' \ + --add-dynamic-module=../ngx-http-auth-jwt-module \ + ${BUILD_FLAGS} + # --with-openssl=/usr/local \ +` +RUN make modules +RUN make install +WORKDIR /usr/lib64/nginx/modules +RUN cp /root/build/nginx/objs/ngx_http_auth_jwt_module.so . +RUN rm -rf /root/build +RUN adduser --system --no-create-home --shell /bin/false --group --disabled-login nginx +RUN mkdir -p /var/cache/nginx /var/log/nginx +WORKDIR /etc/nginx + +FROM ngx_http_auth_jwt_builder_module AS ngx_http_auth_jwt_nginx +LABEL maintainer="TeslaGov" email="developers@teslagov.com" +ARG NGINX_VERSION +RUN <<` + set -e + + apt-get update + apt-get install -y libjansson4 libjwt0 + apt-get clean +` +COPY <<` /etc/nginx/nginx.conf +user nginx; +pid /var/run/nginx.pid; + +load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; + +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + log_format main '$$remote_addr - $$remote_user [$$time_local] "$$request" ' + '$$status $$body_bytes_sent "$$http_referer" ' + '"$$http_user_agent" "$$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + include conf.d/*.conf; +} +` +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/openssl.dockerfile b/openssl.dockerfile new file mode 100644 index 0000000..45140cc --- /dev/null +++ b/openssl.dockerfile @@ -0,0 +1,37 @@ +ARG BASE_IMAGE + +FROM ${BASE_IMAGE} +ARG SRC_DIR=/tmp/openssl-src +ARG OUT_DIR=/usr/local/.openssl +ARG SSL_VERSION +RUN <<` + set -e + apt-get update + apt-get install -y curl build-essential libssl-dev libz-dev + apt-get remove -y openssl + apt-get clean +` +WORKDIR ${SRC_DIR} +RUN <<` + set -e + curl --silent -O https://www.openssl.org/source/openssl-${SSL_VERSION}.tar.gz + tar -xf openssl-${SSL_VERSION}.tar.gz --strip-components=1 +` +RUN ./config --prefix=${OUT_DIR} --openssldir=${OUT_DIR} shared zlib +RUN <<` + set -e + make + make test + make install +` +RUN <<` + set -e + echo "${OUT_DIR}/lib" > /etc/ld.so.conf.d/openssl-${SSL_VERSION}.conf + ldconfig + + ln -sf ${OUT_DIR}/bin/openssl /usr/bin/openssl + ln -sf ${OUT_DIR}/lib64/libssl.so.3 /lib/x86_64-linux-gnu/libssl.so.3 + ln -sf ${OUT_DIR}/lib64/libcrypto.so.3 /lib/x86_64-linux-gnu/libcrypto.so.3 +` +WORKDIR / +#RUN rm -rf ${SRC_DIR} \ No newline at end of file diff --git a/scripts.sh b/scripts.sh index e3fb254..421a4cb 100755 --- a/scripts.sh +++ b/scripts.sh @@ -1,40 +1,72 @@ #!/bin/bash -eu +MAGENTA='\u001b[35m' BLUE='\033[0;34m' GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' +# supported SSL versions +SSL_VERSION_1_1_1w='1.1.1w' +SSL_VERSION_3_0_11='3.0.11' +SSL_VERSION_3_2_1='3.2.1' +SSL_VERSIONS=(${SSL_VERSION_3_2_1}) +SSL_VERSION=${SSL_VERSION:-$SSL_VERSION_3_0_11} + +declare -A SSL_IMAGE_MAP +SSL_IMAGE_MAP[$SSL_VERSION_1_1_1w]="bullseye-slim:openssl-${SSL_VERSION_1_1_1w}" +SSL_IMAGE_MAP[$SSL_VERSION_3_0_11]="bookworm-slim:openssl-${SSL_VERSION_3_0_11}" +SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" + # supported NGINX versions -- for binary distribution -NGINX_VERSION_MAINLINE='1.25.4' +NGINX_VERSION_LEGACY_1='1.20.2' +NGINX_VERSION_LEGACY_2='1.22.1' NGINX_VERSION_STABLE='1.24.0' -NGINX_VERSION_LEGACY_1='1.22.1' -NGINX_VERSION_LEGACY_2='1.20.2' +NGINX_VERSION_MAINLINE='1.25.4' +NGINX_VERSIONS=(${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_MAINLINE}) +NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} -export ORG_NAME=${ORG_NAME:-teslagov} -export IMAGE_NAME=${IMAGE_NAME:-jwt-nginx} -export FULL_IMAGE_NAME=${ORG_NAME}/${IMAGE_NAME} -export CONTAINER_NAME_PREFIX=${CONTAINER_NAME_PREFIX:-jwt-nginx-test} -export NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} +IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} +FULL_IMAGE_NAME=${ORG_NAME:-teslagov}/${IMAGE_NAME} + +TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" all() { build_module - build_test_runner - test + build_test + test_all +} + +verify_and_build_base_image() { + local image=${SSL_IMAGE_MAP[$SSL_VERSION]} + local baseImage=${image%%:*} + + if [ -z ${image} ]; then + echo "Base image not set for SSL version :${SSL_VERSION}" + exit 1 + else + printf "${MAGENTA}Building base image for SSL ${SSL_VERSION}...${NC}\n" + docker image build \ + --build-arg BASE_IMAGE=debian:${baseImage} \ + --build-arg SSL_VERSION=${SSL_VERSION} \ + -f openssl.dockerfile \ + -t ${image} . + fi } build_module() { local dockerArgs=${1:-} - local sourceHash=$(get_hash config src/*) - - printf "${BLUE}Pulling images...${NC}\n" - docker image pull debian:bullseye-slim - docker image pull nginx:${NGINX_VERSION} + local baseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} + + verify_and_build_base_image - printf "${BLUE}Building module for NGINX ${NGINX_VERSION}...${NC}\n" - docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} ${dockerArgs} \ + printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}...${NC}\n" + docker image build \ + -f nginx.dockerfile \ + -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ + --build-arg BASE_IMAGE=${baseImage} \ --build-arg NGINX_VERSION=${NGINX_VERSION} \ - --build-arg SOURCE_HASH=${sourceHash} . + ${dockerArgs} . if [ "$?" -ne 0 ]; then printf "${RED}✘ Build failed ${NC}\n" @@ -55,7 +87,7 @@ clean_module() { start_nginx() { local port=$(get_port) - printf "${BLUE}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" + printf "${MAGENTA}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME} >/dev/null } @@ -72,7 +104,7 @@ cp_bin() { stopContainer=1 fi - printf "${BLUE}Copying binaries to: ${destDir}${NC}\n" + printf "${MAGENTA}Copying binaries to: ${destDir}${NC}\n" rm -rf ${destDir}/* mkdir -p ${destDir} docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ @@ -81,7 +113,7 @@ cp_bin() { usr/lib/x86_64-linux-gnu/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null if [ $stopContainer ]; then - printf "${BLUE}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" + printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" stop_nginx fi } @@ -93,7 +125,7 @@ make_release() { NGINX_VERSION=${2} - printf "${BLUE}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" + printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" rebuild_module rebuild_test_runner @@ -110,61 +142,88 @@ make_release() { # See: https://nginx.org/en/download.html make_releases() { local moduleVersion=$(git describe --tags --abbrev=0) - local nginxVersions=(${NGINX_VERSION_MAINLINE} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2}) rm -rf release/* - for v in ${nginxVersions[@]}; do + for v in ${NGINX_VERSIONS[@]}; do make_release ${moduleVersion} ${v} done } - -build_test_runner() { +build_test() { local dockerArgs=${1:-} - local configHash=$(get_hash $(find test -type f -not -name 'test.sh' -not -name '*.yml' -not -name 'Dockerfile*')) - local sourceHash=$(get_hash test/test.sh) local port=$(get_port) + local sslPort=$(get_port $((port + 1))) + local runnerBaseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} - printf "${BLUE}Building test runner using port ${port}...${NC}\n" - docker compose -f ./test/docker-compose-test.yml build ${dockerArgs} \ - --build-arg CONFIG_HASH=${configHash}\ - --build-arg SOURCE_HASH=${sourceHash} \ - --build-arg PORT=${port} + export TEST_CONTAINER_NAME_PREFIX + export FULL_IMAGE_NAME + export NGINX_VERSION + + printf "${MAGENTA}Building test NGINX & runner using port ${port}...${NC}\n" + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ./test/docker-compose-test.yml build \ + --build-arg RUNNER_BASE_IMAGE=${runnerBaseImage} \ + --build-arg PORT=${port} \ + --build-arg SSL_PORT=${sslPort} \ + ${dockerArgs} } -rebuild_test_runner() { - build_test_runner --no-cache +rebuild_test() { + build_test --no-cache +} + +test_all() { + for SSL_VERSION in "${SSL_VERSIONS[@]}"; do + for NGINX_VERSION in "${NGINX_VERSIONS[@]}"; do + test + done + done } test() { - build_test_runner + build_module + build_test - printf "${BLUE}Running tests...${NC}\n" - docker compose -f ./test/docker-compose-test.yml up --no-start - docker start ${CONTAINER_NAME_PREFIX} - - if [ "$(docker container inspect -f '{{.State.Running}}' ${CONTAINER_NAME_PREFIX})" != "true" ]; then - printf "${RED}Failed to start NGINX test container. See logs below:\n" - docker logs ${CONTAINER_NAME_PREFIX} - printf "${NC}\n" - else - test_now - fi + printf "${MAGENTA}Running tests...${NC}\n" + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ./test/docker-compose-test.yml up \ + --no-start + + + trap 'docker compose -f ./test/docker-compose-test.yml down' 0 - docker compose -f ./test/docker-compose-test.yml down + test_now } test_now() { - docker start -a ${CONTAINER_NAME_PREFIX}-runner -} + nginxContainerName="${TEST_CONTAINER_NAME_PREFIX}-nginx" + runnerContainerName="${TEST_CONTAINER_NAME_PREFIX}-runner" -get_hash() { - sha1sum $@ | sed -E 's|\s+|:|' | tr '\n' ' ' | sha1sum | head -c 40 + docker start ${nginxContainerName} + + if [ "$(docker container inspect -f '{{.State.Running}}' ${nginxContainerName})" != "true" ]; then + printf "${RED}Failed to start container \"${nginxContainerName}\". See logs below:\n" + docker logs ${nginxContainerName} + printf "${NC}\n" + return + fi + + docker start -a ${runnerContainerName} + + echo + echo "Tests were executed with the following options:" + echo " SSL Version: ${SSL_VERSION}" + echo " NGINX Version: ${NGINX_VERSION}" } get_port() { - for p in $(seq 8000 8100); do + startPort=${1:-8000} + endPort=$((startPort + 100)) + + for p in $(seq ${startPort} ${endPort}); do if ! ss -ln | grep -q ":${p} "; then echo ${p} break diff --git a/test/Dockerfile-test-nginx b/test/Dockerfile-test-nginx deleted file mode 100644 index 5f01436..0000000 --- a/test/Dockerfile-test-nginx +++ /dev/null @@ -1,15 +0,0 @@ -ARG BASE_IMAGE -ARG CONFIG_HASH -ARG PORT - -FROM ${BASE_IMAGE} as NGINX -ARG CONFIG_HASH -ARG PORT -RUN echo "Config Hash: ${CONFIG_HASH}" -COPY /docker-entrypoint.d/* /docker-entrypoint.d/ -COPY /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.conf -COPY /etc/nginx/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf -COPY /etc/nginx/ec_key_256-pub.pem /etc/nginx/ec-256-key.conf -COPY /etc/nginx/ec_key_384-pub.pem /etc/nginx/ec-384-key.conf -COPY /etc/nginx/ec_key_521-pub.pem /etc/nginx/ec-521-key.conf -RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf diff --git a/test/Dockerfile-test-runner b/test/Dockerfile-test-runner deleted file mode 100644 index c8cbff2..0000000 --- a/test/Dockerfile-test-runner +++ /dev/null @@ -1,13 +0,0 @@ -ARG SOURCE_HASH -ARG PORT - -FROM alpine:3.7 AS test-base -RUN apk add curl bash - -FROM test-base AS test -ARG SOURCE_HASH -ARG PORT -ENV PORT=${PORT} -RUN echo "Source Hash: ${SOURCE_HASH}" -COPY test.sh . -CMD ./test.sh ${PORT} diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index eff2460..3c0e9be 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -3,23 +3,20 @@ version: '3.3' services: nginx: - container_name: ${CONTAINER_NAME_PREFIX} + container_name: ${TEST_CONTAINER_NAME_PREFIX}-nginx build: context: . - dockerfile: Dockerfile-test-nginx + dockerfile: test-nginx.dockerfile args: - BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} - command: [nginx-debug, '-g', 'daemon off;'] + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION} logging: driver: ${LOG_DRIVER:-journald} runner: - container_name: ${CONTAINER_NAME_PREFIX}-runner + container_name: ${TEST_CONTAINER_NAME_PREFIX}-runner build: context: . - dockerfile: Dockerfile-test-runner - environment: - BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} + dockerfile: test-runner.dockerfile depends_on: - nginx \ No newline at end of file diff --git a/test/docker-entrypoint.d/10-nginx-test.sh b/test/docker-entrypoint.d/10-nginx-test.sh deleted file mode 100755 index 0bf8791..0000000 --- a/test/docker-entrypoint.d/10-nginx-test.sh +++ /dev/null @@ -1 +0,0 @@ -nginx -t \ No newline at end of file diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 3421b5b..229d545 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -3,7 +3,13 @@ access_log /var/log/nginx/access.log; server { listen %{PORT}; + listen %{SSL_PORT} ssl; server_name localhost; + + ssl_certificate /etc/nginx/test.crt; + ssl_certificate_key /etc/nginx/test.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; auth_jwt_loginurl "https://example.com/login"; @@ -212,7 +218,7 @@ vXjq39xtcIBRTO1c2zs= auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS256; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -224,7 +230,7 @@ vXjq39xtcIBRTO1c2zs= auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS384; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -236,7 +242,7 @@ vXjq39xtcIBRTO1c2zs= auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS512; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -248,7 +254,7 @@ vXjq39xtcIBRTO1c2zs= auth_jwt_location HEADER=Authorization; auth_jwt_algorithm ES256; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/ec-256-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/ec_key_256-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -260,7 +266,7 @@ vXjq39xtcIBRTO1c2zs= auth_jwt_location HEADER=Authorization; auth_jwt_algorithm ES384; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/ec-384-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/ec_key_384-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -272,7 +278,7 @@ vXjq39xtcIBRTO1c2zs= auth_jwt_location HEADER=Authorization; auth_jwt_algorithm ES512; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/ec-521-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/ec_key_521-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -390,4 +396,3 @@ vXjq39xtcIBRTO1c2zs= } } } - diff --git a/test/etc/nginx/test.crt b/test/etc/nginx/test.crt new file mode 100644 index 0000000..fb406ba --- /dev/null +++ b/test/etc/nginx/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIUMG9M4Itu0cOyX0+La+7huiIoX6YwDQYJKoZIhvcNAQEL +BQAwcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQHDAxG +YWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4xFzAV +BgNVBAsMDk5HSU5YIEF1dGggSldUMB4XDTI0MDMxNTE4MTM1MloXDTM0MDMxMzE4 +MTM1MlowcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQH +DAxGYWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4x +FzAVBgNVBAsMDk5HSU5YIEF1dGggSldUMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAih41Ct5XgcSTz7ZVAjBb0t0z9Qae08aseoMEKJf7AmNqKtsvzeAw +/DJxOWJR5VPtUWhFAmXxPfG2B6aiSIVJVpG9yzcdQlCvyJG7Ub4QCm5GXwpU+zDC +qmD5ksz9QMdOzvRLypAU1ciZiCXjwpUnW+BZyZ9Tpmsxm6/gOzkd3rxoIbc9uXxp +5o4n6k02EPSzLzUhkZnhLQrOAGUB7+q11FAU5eNMlTWC9gQUsbNaTVtKmM2eV9BA +UHdX2GbkfFbN22l3Wey4oyNZWmye1ZFOPyBR+tyU3pofhb+R+hTFmeNBzrJq3i30 +Qi0B8AnulKdOjnTysPYjDTrN6xcVDWNmPQIDAQABo1MwUTAdBgNVHQ4EFgQUczdy +7s64NJHNGsQTf/zwFnQe6LMwHwYDVR0jBBgwFoAUczdy7s64NJHNGsQTf/zwFnQe +6LMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAfcxCiz6ShHof +lXiE2j+s556SM2n8oW/S1BSjFC2wF1uKVeMJA1gAaWObC3ElqffFlqTdCorhgRS/ +knWa+Sqe/jWBSgwLG/e5DvxXWjD7b7kZdAZNy9evs5nhVfcLT+GyvB/z5GdAFY7s +xYmLrC07ubhHIL9h7lhNKbRr++o+BcClQBZKRO4fxBwXxqx/rHudjH87Wr61Ov52 +90xNjwcqvevY0skmPao5+oyxkURdKZualNxiOGMPpywkpJkfl8Az5xKAJhUMAtFR +smhQduejEkcxfxtsiYgVoulI29GAsMr9zHps9zb5k0+SWIiSixjQ0CpRhLcNYu4F +QPgLQLGwUQ== +-----END CERTIFICATE----- diff --git a/test/etc/nginx/test.key b/test/etc/nginx/test.key new file mode 100644 index 0000000..13ec754 --- /dev/null +++ b/test/etc/nginx/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCKHjUK3leBxJPP +tlUCMFvS3TP1Bp7Txqx6gwQol/sCY2oq2y/N4DD8MnE5YlHlU+1RaEUCZfE98bYH +pqJIhUlWkb3LNx1CUK/IkbtRvhAKbkZfClT7MMKqYPmSzP1Ax07O9EvKkBTVyJmI +JePClSdb4FnJn1OmazGbr+A7OR3evGghtz25fGnmjifqTTYQ9LMvNSGRmeEtCs4A +ZQHv6rXUUBTl40yVNYL2BBSxs1pNW0qYzZ5X0EBQd1fYZuR8Vs3baXdZ7LijI1la +bJ7VkU4/IFH63JTemh+Fv5H6FMWZ40HOsmreLfRCLQHwCe6Up06OdPKw9iMNOs3r +FxUNY2Y9AgMBAAECggEAAkwEggGp/xb67FCyDJ8rdimTZFPi9U7coUCN8HNI/qrf +lTnfvox0oOUUqMMmIIQeS/HJ4ANvZe8GO3QkE8R5Sg7F0yjZL2tyTCNPgOMCMK8E +mmHS58brHdrbm658C1ILnfmssjNmNueNbuW00Koa8imCsY2ZEW+L7vTKuMFqg6c+ +BDJxC4yoCPwSTVfcajjzI6FVfphE0pd8Ho/sE8vTqdmovh23+vgfNUq1L9Smvf7R +YLM+hS1ouRP2BI5AN0sm04Kxd8MKPzuwCxteoZ9Y9YHyr1JeWGTTL0T24+LwUee/ +24zXZFrzpTgmtDYeEuVWsF5bP/fMS4Fctda3pdJMsQKBgQDCANjGDwwfSCCev2kl +WdrFJywhn5hWLWFwlo/FwLOsFJtejaBwIDRQCMPZ74H+KMHwUnO3vTanKJWqDRP9 +CdMh94C1BqobRV6rN4HgA4Opxim1EyRWHV6ui41zokk2mJrwUzKkR8t9lt9EZKrk +ZPyKER9A4hBqBmYvaYxodN8U1QKBgQC2QXUQq9j7niT7t4xMi0e9vnPLs0z1yUK9 +0nzKwTHDPflk3o2sKvH7199qVkc15JQ9DQ7NuYD7ezLbE3DJuVzpNDAfNXmfWHmp +7ukdnxyn6ZCmzQY7/fTpJTEGKVQMVCgf2f5ANgxm5EmN0yWRMcEt1VXIwCisY56p +o6nwv/1fyQKBgQCJBnIVyjEEszwfBBEvCX0kvVtFUGUXkSv+isl3onkFNPTcXuoP +6B8q3FYAy1MkggMhTAthnqpIfLjhCCWzFspidl8Y/WEOq/uGsUjxQWowcr+onqGO +lWX3oKfDIb/WaQkeb5UYRYFr7jE6LGQrt0xL9HX/rOxtBqIMIN/EM7ARFQKBgDAJ +zMtaIFUh9+mJFafPRleS7X6RggV+yOKzqkTe6zjlCuk1Z+4rW6Df43lpyFdCKnh1 +CqPa805VyK/Jzf69pumo4c44EBiZ/2d1G2i9WZZAj+oHPE9vvq/9J5DSL98YB4Nt +uABAvsAYB/Mj5lEA5kQoaPYDADWABH/+LXrRf/1RAoGAUvxPvmpkGMC+KdmjLam7 +CPC3+y4MZOyZ11BhOxLhd1K2qcQd9K7tkjUhNxRn5GVzpzOKeFJFtiih2uN+PBNJ +oylPR03uk/7D52b1OYaJhs9bQkth//Qk935nyRM26C2vG4tQLfT/cFi5F53n0ZCQ +7e8O6+QY0lZnpvsfnt8YIsM= +-----END PRIVATE KEY----- diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile new file mode 100644 index 0000000..c4a6104 --- /dev/null +++ b/test/test-nginx.dockerfile @@ -0,0 +1,18 @@ +ARG BASE_IMAGE +ARG PORT +ARG SSL_PORT + +FROM ${BASE_IMAGE} as NGINX +ARG PORT +ARG SSL_PORT +COPY etc/ /etc/ +RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf +RUN sed -i "s|%{SSL_PORT}|${SSL_PORT}|" /etc/nginx/conf.d/test.conf +COPY <<` /usr/share/nginx/html/index.html + + Test + +

NGINX Auth-JWT Module Test

+ + +` diff --git a/test/test-runner.dockerfile b/test/test-runner.dockerfile new file mode 100644 index 0000000..0aca095 --- /dev/null +++ b/test/test-runner.dockerfile @@ -0,0 +1,16 @@ +ARG RUNNER_BASE_IMAGE +ARG PORT +ARG SSL_PORT + +FROM ${RUNNER_BASE_IMAGE} +ARG PORT +ARG SSL_PORT +ENV PORT=${PORT} +ENV SSL_PORT=${SSL_PORT} +RUN <<` + set -e + apt-get update + apt-get install -y curl bash +` +COPY test.sh . +CMD ./test.sh ${PORT} ${SSL_PORT} diff --git a/test/test.sh b/test/test.sh index 5671b8f..f54e0de 100755 --- a/test/test.sh +++ b/test/test.sh @@ -1,7 +1,6 @@ -#!/bin/bash -u +#!/bin/bash -eu # set a test # here to execute only that test and output additional info -PORT=${1:-8000} DEBUG= RED='\e[31m' @@ -18,35 +17,40 @@ run_test () { if [ "${DEBUG}" == '' ] || [ ${DEBUG} == ${NUM_TESTS} ]; then local OPTIND; - local name='' - local path='' - local expectedCode='' - local expectedResponseRegex='' - local extraCurlOpts='' - local curlCommand='' - local exitCode='' - local response='' + local name= + local path= + local expectedCode= + local expectedResponseRegex= + local extraCurlOpts= + local scheme='http' + local port=${PORT} + local curlCommand= + local exitCode= + local response= local testNum="${GRAY}${NUM_TESTS}${NC}\t" - while getopts "n:p:r:c:x:" option; do + while getopts "n:sp:r:c:x:" option; do case $option in - n) - name=$OPTARG;; - p) - path=$OPTARG;; - c) - expectedCode=$OPTARG;; - r) - expectedResponseRegex=$OPTARG;; - x) - extraCurlOpts=$OPTARG;; - \?) # Invalid option - printf "Error: Invalid option\n"; - exit;; + n) + name=$OPTARG;; + s) + scheme='https' + port=${SSL_PORT};; + p) + path=$OPTARG;; + c) + expectedCode=$OPTARG;; + r) + expectedResponseRegex=$OPTARG;; + x) + extraCurlOpts=$OPTARG;; + \?) # Invalid option + printf "Error: Invalid option\n"; + exit;; esac done - curlCommand="curl -s -v http://nginx:${PORT}${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" + curlCommand="curl -skv ${scheme}://nginx:${port}${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" response=$(eval "${curlCommand}") exitCode=$? @@ -108,10 +112,20 @@ main() { run_test -n 'when auth disabled, should return 200' \ -p '/' \ -c '200' + + run_test -s \ + -n '[SSL] when auth disabled, should return 200' \ + -p '/' \ + -c '200' run_test -n 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ -p '/secure/auth-header/default' \ -c '302' + + run_test -n '[SSL] when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + -s \ + -p '/secure/auth-header/default' \ + -c '302' run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 200' \ -p '/secure/auth-header/default/no-redirect' \ From 02f4e17eb84dd2de78d5cedca8f0b4f8247470e3 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 28 Aug 2024 12:12:07 -0400 Subject: [PATCH 104/130] case-insenitive Bearer check #134 (#135) --- openssl.dockerfile | 14 +++++++------- scripts.sh | 6 +++--- src/ngx_http_auth_jwt_module.c | 2 +- test/docker-compose-test.yml | 2 -- test/test.sh | 6 ++++++ 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/openssl.dockerfile b/openssl.dockerfile index 45140cc..d8bb293 100644 --- a/openssl.dockerfile +++ b/openssl.dockerfile @@ -1,9 +1,9 @@ -ARG BASE_IMAGE +ARG BASE_IMAGE=debian:bookworm-slim FROM ${BASE_IMAGE} -ARG SRC_DIR=/tmp/openssl-src -ARG OUT_DIR=/usr/local/.openssl -ARG SSL_VERSION +ARG SSL_VERSION=3.2.1 +ENV SRC_DIR=/tmp/openssl-src +ENV OUT_DIR=/usr/local/.openssl RUN <<` set -e apt-get update @@ -13,8 +13,8 @@ RUN <<` ` WORKDIR ${SRC_DIR} RUN <<` - set -e - curl --silent -O https://www.openssl.org/source/openssl-${SSL_VERSION}.tar.gz + set -ex + curl --silent -LO https://www.openssl.org/source/openssl-${SSL_VERSION}.tar.gz tar -xf openssl-${SSL_VERSION}.tar.gz --strip-components=1 ` RUN ./config --prefix=${OUT_DIR} --openssldir=${OUT_DIR} shared zlib @@ -34,4 +34,4 @@ RUN <<` ln -sf ${OUT_DIR}/lib64/libcrypto.so.3 /lib/x86_64-linux-gnu/libcrypto.so.3 ` WORKDIR / -#RUN rm -rf ${SRC_DIR} \ No newline at end of file +RUN rm -rf ${SRC_DIR} \ No newline at end of file diff --git a/scripts.sh b/scripts.sh index 421a4cb..6f109d9 100755 --- a/scripts.sh +++ b/scripts.sh @@ -40,13 +40,13 @@ all() { verify_and_build_base_image() { local image=${SSL_IMAGE_MAP[$SSL_VERSION]} local baseImage=${image%%:*} - + if [ -z ${image} ]; then echo "Base image not set for SSL version :${SSL_VERSION}" exit 1 else - printf "${MAGENTA}Building base image for SSL ${SSL_VERSION}...${NC}\n" - docker image build \ + printf "${MAGENTA}Building ${baseImage} base image for SSL ${SSL_VERSION}...${NC}\n" + docker buildx build \ --build-arg BASE_IMAGE=debian:${baseImage} \ --build-arg SSL_VERSION=${SSL_VERSION} \ -f openssl.dockerfile \ diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 85a646d..e21560c 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -630,7 +630,7 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) { static const char *BEARER_PREFIX = "Bearer "; - if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) + if (ngx_strncasecmp(jwtHeaderVal->value.data, (u_char *)BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) { ngx_str_t jwtHeaderValWithoutBearer = jwtHeaderVal->value; diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index 3c0e9be..14c88da 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -1,5 +1,3 @@ -version: '3.3' - services: nginx: diff --git a/test/test.sh b/test/test.sh index f54e0de..2bf9cb3 100755 --- a/test/test.sh +++ b/test/test.sh @@ -143,6 +143,12 @@ main() { -r "< Test-Authorization: Bearer ${JWT_HS256_VALID}" \ -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + run_test -n 'when auth enabled with Authorization header with Bearer, lower-case "bearer" should be accepted' \ + -p '/secure/auth-header/default/proxy-header' \ + -c '200' \ + -r "< Test-Authorization: bearer ${JWT_HS256_VALID}" \ + -x "--header \"Authorization: bearer ${JWT_HS256_VALID}\"" + run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ -p '/secure/cookie/default' \ -c '302' From 272c02e2302208993df4f99a7f1751a793d16662 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 28 Aug 2024 12:31:26 -0400 Subject: [PATCH 105/130] fix release script --- scripts.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts.sh b/scripts.sh index 6f109d9..f6863f0 100755 --- a/scripts.sh +++ b/scripts.sh @@ -61,7 +61,7 @@ build_module() { verify_and_build_base_image printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}...${NC}\n" - docker image build \ + docker buildx build \ -f nginx.dockerfile \ -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ --build-arg BASE_IMAGE=${baseImage} \ @@ -88,7 +88,7 @@ start_nginx() { local port=$(get_port) printf "${MAGENTA}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" - docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME} >/dev/null + docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME}:${NGINX_VERSION} >/dev/null } stop_nginx() { @@ -128,7 +128,7 @@ make_release() { printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" rebuild_module - rebuild_test_runner + rebuild_test test cp_bin From c5882b0c2106bc478af174bb36ea956a99038127 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 14 Oct 2024 10:43:53 -0400 Subject: [PATCH 106/130] fix Docker warnings --- nginx.dockerfile | 6 ++---- test/test-nginx.dockerfile | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/nginx.dockerfile b/nginx.dockerfile index 21c7460..c0e6c02 100644 --- a/nginx.dockerfile +++ b/nginx.dockerfile @@ -1,16 +1,14 @@ ARG BASE_IMAGE ARG NGINX_VERSION - -FROM ${BASE_IMAGE} as ngx_http_auth_jwt_builder_base +FROM ${BASE_IMAGE} AS ngx_http_auth_jwt_builder_base LABEL stage=ngx_http_auth_jwt_builder RUN <<` apt-get update apt-get install -y curl build-essential ` - -FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module +FROM ngx_http_auth_jwt_builder_base AS ngx_http_auth_jwt_builder_module LABEL stage=ngx_http_auth_jwt_builder ENV PATH "${PATH}:/etc/nginx" ENV LD_LIBRARY_PATH=/usr/local/lib diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile index c4a6104..e12acb4 100644 --- a/test/test-nginx.dockerfile +++ b/test/test-nginx.dockerfile @@ -2,7 +2,7 @@ ARG BASE_IMAGE ARG PORT ARG SSL_PORT -FROM ${BASE_IMAGE} as NGINX +FROM ${BASE_IMAGE} AS NGINX ARG PORT ARG SSL_PORT COPY etc/ /etc/ From 867562a318abb5b6997d77300f30b942e7e964ff Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 14 Oct 2024 10:44:06 -0400 Subject: [PATCH 107/130] fix /tmp dir perms for containers --- nginx.dockerfile | 1 + openssl.dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/nginx.dockerfile b/nginx.dockerfile index c0e6c02..360469b 100644 --- a/nginx.dockerfile +++ b/nginx.dockerfile @@ -3,6 +3,7 @@ ARG NGINX_VERSION FROM ${BASE_IMAGE} AS ngx_http_auth_jwt_builder_base LABEL stage=ngx_http_auth_jwt_builder +RUN chmod 1777 /tmp RUN <<` apt-get update apt-get install -y curl build-essential diff --git a/openssl.dockerfile b/openssl.dockerfile index d8bb293..42f824f 100644 --- a/openssl.dockerfile +++ b/openssl.dockerfile @@ -4,6 +4,7 @@ FROM ${BASE_IMAGE} ARG SSL_VERSION=3.2.1 ENV SRC_DIR=/tmp/openssl-src ENV OUT_DIR=/usr/local/.openssl +RUN chmod 1777 /tmp RUN <<` set -e apt-get update From b93b816c97937e6a60620165688f937bba8e4f91 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 14 Oct 2024 10:44:19 -0400 Subject: [PATCH 108/130] update NGINX versions to build against --- scripts.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts.sh b/scripts.sh index f6863f0..2ae02d4 100755 --- a/scripts.sh +++ b/scripts.sh @@ -21,9 +21,10 @@ SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" # supported NGINX versions -- for binary distribution NGINX_VERSION_LEGACY_1='1.20.2' NGINX_VERSION_LEGACY_2='1.22.1' -NGINX_VERSION_STABLE='1.24.0' -NGINX_VERSION_MAINLINE='1.25.4' -NGINX_VERSIONS=(${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_MAINLINE}) +NGINX_VERSION_LEGACY_3='1.24.0' +NGINX_VERSION_STABLE='1.26.2' +NGINX_VERSION_MAINLINE='1.27.2' +NGINX_VERSIONS=(${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2} ${NGINX_VERSION_LEGACY_3} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_MAINLINE}) NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} From e8e60e652a14474dcef39fac95c79385813737a8 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 14 Oct 2024 10:44:53 -0400 Subject: [PATCH 109/130] rm redundant `-e` --- scripts.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts.sh b/scripts.sh index 2ae02d4..5c9b785 100755 --- a/scripts.sh +++ b/scripts.sh @@ -120,8 +120,6 @@ cp_bin() { } make_release() { - set -e - local moduleVersion=${1} NGINX_VERSION=${2} From 16b0369e8c369809de8bda8004c65b5d36fd2d56 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Mon, 14 Oct 2024 10:45:06 -0400 Subject: [PATCH 110/130] update scripts to support arts --- scripts.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts.sh b/scripts.sh index 5c9b785..4b46a85 100755 --- a/scripts.sh +++ b/scripts.sh @@ -233,7 +233,8 @@ get_port() { if [ $# -eq 0 ]; then all else - for fn in "$@"; do - ${fn} - done + fn=$1 + shift + + ${fn} "$@" fi From d8974ebd93539d7961fdc552cf563701961a16a3 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 10:03:35 -0500 Subject: [PATCH 111/130] no releases from PRs --- .github/workflows/make-releases.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 037823b..3a12f70 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -6,11 +6,6 @@ on: - master paths: - src/** - pull_request: - branches: - - master - paths: - - src/** workflow_dispatch: jobs: From 27fcd3d6fbfb561966d7e03a70000566123aee17 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 10:03:53 -0500 Subject: [PATCH 112/130] update NGINX versions to build against --- .github/workflows/make-releases.yml | 2 +- scripts.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 3a12f70..fdc5591 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: # NGINX versions to build/test against - nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.25.3'] + nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.26.2', '1.27.3'] # The following versions of libjwt are compatible: # * v1.0 - v1.12.0 diff --git a/scripts.sh b/scripts.sh index 4b46a85..af59e9b 100755 --- a/scripts.sh +++ b/scripts.sh @@ -23,7 +23,7 @@ NGINX_VERSION_LEGACY_1='1.20.2' NGINX_VERSION_LEGACY_2='1.22.1' NGINX_VERSION_LEGACY_3='1.24.0' NGINX_VERSION_STABLE='1.26.2' -NGINX_VERSION_MAINLINE='1.27.2' +NGINX_VERSION_MAINLINE='1.27.3' NGINX_VERSIONS=(${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2} ${NGINX_VERSION_LEGACY_3} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_MAINLINE}) NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} From 576fe71f0a693d5e380069a316ae5361fe73af30 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 10:04:03 -0500 Subject: [PATCH 113/130] update workflow action version --- .github/workflows/make-releases.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index fdc5591..7316e6b 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -94,7 +94,7 @@ jobs: tar czf ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz ngx_http_auth_jwt_module.so - name: Upload build artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: if-no-files-found: error name: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz From d29adbb2ced992961d8c9c01b3dcf162b4c3f851 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 10:15:51 -0500 Subject: [PATCH 114/130] rename start/stop script functions --- scripts.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts.sh b/scripts.sh index af59e9b..edbb186 100755 --- a/scripts.sh +++ b/scripts.sh @@ -85,14 +85,14 @@ clean_module() { docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true } -start_nginx() { +start() { local port=$(get_port) printf "${MAGENTA}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME}:${NGINX_VERSION} >/dev/null } -stop_nginx() { +stop() { docker stop "${IMAGE_NAME}" >/dev/null } From a774c4208141b77d3144ac1cd296a420fbfe4b85 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 10:16:09 -0500 Subject: [PATCH 115/130] rename scripts.sh --- scripts.sh => scripts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts.sh => scripts (100%) diff --git a/scripts.sh b/scripts similarity index 100% rename from scripts.sh rename to scripts From 7c9cb00f5e3bf86192c6fc7a2b6a11a706923106 Mon Sep 17 00:00:00 2001 From: Adrian Carreno Date: Tue, 4 Feb 2025 12:30:37 -0300 Subject: [PATCH 116/130] Feature: Add support for ARM64 (#139) --- openssl.dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openssl.dockerfile b/openssl.dockerfile index 42f824f..9839d29 100644 --- a/openssl.dockerfile +++ b/openssl.dockerfile @@ -31,8 +31,8 @@ RUN <<` ldconfig ln -sf ${OUT_DIR}/bin/openssl /usr/bin/openssl - ln -sf ${OUT_DIR}/lib64/libssl.so.3 /lib/x86_64-linux-gnu/libssl.so.3 - ln -sf ${OUT_DIR}/lib64/libcrypto.so.3 /lib/x86_64-linux-gnu/libcrypto.so.3 + ln -sf ${OUT_DIR}/lib64/libssl.so.3 /lib/$(uname -m)-linux-gnu/libssl.so.3 + ln -sf ${OUT_DIR}/lib64/libcrypto.so.3 /lib/$(uname -m)-linux-gnu/libcrypto.so.3 ` WORKDIR / RUN rm -rf ${SRC_DIR} \ No newline at end of file From edabc23442653cc82568dcd83b034408e4708815 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 10:33:12 -0500 Subject: [PATCH 117/130] support ARM --- scripts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts b/scripts index edbb186..9d90185 100755 --- a/scripts +++ b/scripts @@ -110,8 +110,8 @@ cp_bin() { mkdir -p ${destDir} docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ - usr/lib/x86_64-linux-gnu/libjansson.so.* \ - usr/lib/x86_64-linux-gnu/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null + usr/lib/$(uname -m)-linux-gnu/libjansson.so.* \ + usr/lib/$(uname -m)-linux-gnu/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null if [ $stopContainer ]; then printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" From 81a2b445d20518d264cc32fcb51350ccbf8a7214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Tue, 4 Feb 2025 14:14:30 -0300 Subject: [PATCH 118/130] Support extracting claims to NGINX variables (#145) Co-authored-by: Matt Gilham <7717048+mgilham@users.noreply.github.com> Co-authored-by: Josh McCullough Co-authored-by: Josh McCullough --- README.md | 35 +-- src/ngx_http_auth_jwt_module.c | 387 +++++++++++++++++++++++++------- test/etc/nginx/conf.d/test.conf | 44 ++++ test/test.sh | 34 ++- 4 files changed, 393 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index ba3dde1..95c83af 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,20 @@ This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. -| Directive | Description | -| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `auth_jwt_key` | The key to use to decode/verify the JWT, *in binhex format* -- see below. | -| `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | -| `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | -| `auth_jwt_enabled` | Set to "on" to enable JWT checking. | -| `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | -| `auth_jwt_location` | Indicates where the JWT is located in the request -- see below. | -| `auth_jwt_validate_sub` | Set to "on" to validate the `sub` claim (e.g. user id) in the JWT. | -| `auth_jwt_extract_request_claims` | Set to a space-delimited list of claims to extract from the JWT and set as request headers. These will be accessible via e.g: `$http_jwt_sub` | -| `auth_jwt_extract_response_claims` | Set to a space-delimited list of claims to extract from the JWT and set as response headers. These will be accessible via e.g: `$sent_http_jwt_sub` | -| `auth_jwt_use_keyfile` | Set to "on" to read the key from a file rather than from the `auth_jwt_key` directive. | -| `auth_jwt_keyfile_path` | Set to the path from which the key should be read when `auth_jwt_use_keyfile` is enabled. | +| Directive | Description | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `auth_jwt_key` | The key to use to decode/verify the JWT, *in binhex format* -- see below. | +| `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | +| `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | +| `auth_jwt_enabled` | Set to "on" to enable JWT checking. | +| `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | +| `auth_jwt_location` | Indicates where the JWT is located in the request -- see below. | +| `auth_jwt_validate_sub` | Set to "on" to validate the `sub` claim (e.g. user id) in the JWT. | +| `auth_jwt_extract_var_claims` | Set to a space-delimited list of claims to extract from the JWT and make available as NGINX variables. These will be accessible via e.g: `$jwt_claim_sub` | +| `auth_jwt_extract_request_claims` | Set to a space-delimited list of claims to extract from the JWT and set as request headers. These will be accessible via e.g: `$http_jwt_sub` | +| `auth_jwt_extract_response_claims` | Set to a space-delimited list of claims to extract from the JWT and set as response headers. These will be accessible via e.g: `$sent_http_jwt_sub` | +| `auth_jwt_use_keyfile` | Set to "on" to read the key from a file rather than from the `auth_jwt_key` directive. | +| `auth_jwt_keyfile_path` | Set to the path from which the key should be read when `auth_jwt_use_keyfile` is enabled. | ## Algorithms @@ -92,19 +93,19 @@ auth_jwt_validate_sub on; You may specify claims to be extracted from the JWT and placed on the request and/or response headers. This is especially handly because the claims will then also be available as NGINX variables. -If you only wish to access a claim as an NGINX variable, you should use `auth_jwt_extract_request_claims` so that the claim does not end up being sent to the client as a response header. However, if you do want the claim to be sent to the client in the response, then use `auth_jwt_extract_response_claims` instead. +If you only wish to access a claim as an NGINX variable, you should use `auth_jwt_extract_var_claims` so that the claim does not end up being sent to the client as a response header. However, if you do want the claim to be sent to the client in the response, you may use `auth_jwt_extract_response_claims` instead. _Please note that `number`, `boolean`, `array`, and `object` claims are not supported at this time -- only `string` claims are supported._ An error will be thrown if you attempt to extract a non-string claim. -### Using Request Claims +### Using Claims For example, you could configure an NGINX location which redirects to the current user's profile. Suppose `sub=abc-123`, the configuration below would redirect to `/profile/abc-123`. ```nginx location /profile/me { - auth_jwt_extract_request_claims sub; + auth_jwt_extract_var_claims sub; - return 301 /profile/$http_jwt_sub; + return 301 /profile/$jwt_claim_sub; } ``` diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index e21560c..fe428b4 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -31,6 +31,7 @@ typedef struct ngx_str_t jwt_location; ngx_str_t algorithm; ngx_flag_t validate_sub; + ngx_array_t *extract_var_claims; ngx_array_t *extract_request_claims; ngx_array_t *extract_response_claims; ngx_str_t keyfile_path; @@ -38,18 +39,28 @@ typedef struct ngx_str_t _keyfile; } auth_jwt_conf_t; +typedef struct +{ + ngx_int_t validation_status; + ngx_array_t *claim_values; +} auth_jwt_ctx_t; + static ngx_int_t init(ngx_conf_t *cf); static void *create_conf(ngx_conf_t *cf); static char *merge_conf(ngx_conf_t *cf, void *parent, void *child); +static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static ngx_int_t get_jwt_var_claim(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); static char *merge_extract_request_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf); +static auth_jwt_ctx_t *get_request_jwt_ctx(ngx_http_request_t *r); static ngx_int_t handle_request(ngx_http_request_t *r); static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt); static int validate_exp(auth_jwt_conf_t *jwtcf, jwt_t *jwt); static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static ngx_int_t extract_var_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt, auth_jwt_ctx_t *ctx); static void extract_request_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); -static ngx_int_t free_jwt_and_redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf); static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf); static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location); @@ -106,6 +117,13 @@ static ngx_command_t auth_jwt_directives[] = { offsetof(auth_jwt_conf_t, validate_sub), NULL}, + {ngx_string("auth_jwt_extract_var_claims"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, + merge_extract_var_claims, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, extract_var_claims), + NULL}, + {ngx_string("auth_jwt_extract_request_claims"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, merge_extract_request_claims, @@ -194,6 +212,7 @@ static void *create_conf(ngx_conf_t *cf) conf->validate_sub = NGX_CONF_UNSET; conf->redirect = NGX_CONF_UNSET; conf->validate_sub = NGX_CONF_UNSET; + conf->extract_var_claims = NULL; conf->extract_request_claims = NULL; conf->extract_response_claims = NULL; conf->use_keyfile = NGX_CONF_UNSET; @@ -213,6 +232,7 @@ static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->algorithm, prev->algorithm, "HS256"); ngx_conf_merge_str_value(conf->keyfile_path, prev->keyfile_path, ""); ngx_conf_merge_off_value(conf->validate_sub, prev->validate_sub, 0); + merge_array(cf->pool, &conf->extract_var_claims, prev->extract_var_claims, sizeof(ngx_str_t)); merge_array(cf->pool, &conf->extract_request_claims, prev->extract_request_claims, sizeof(ngx_str_t)); merge_array(cf->pool, &conf->extract_response_claims, prev->extract_response_claims, sizeof(ngx_str_t)); @@ -252,6 +272,108 @@ static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_OK; } +static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c) +{ + auth_jwt_conf_t *conf = c; + ngx_array_t *claims = conf->extract_var_claims; + + if (claims == NULL) + { + claims = ngx_array_create(cf->pool, 1, sizeof(ngx_str_t)); + conf->extract_var_claims = claims; + } + + ngx_str_t *values = cf->args->elts; + + // start at 1 because the first element is the directive (auth_jwt_extract_var_claims) + for (ngx_uint_t i = 1; i < cf->args->nelts; ++i) + { + // add this claim's name to the config struct + ngx_str_t *element = ngx_array_push(claims); + + *element = values[i]; + + // add an http variable for this claim + size_t var_name_len = 10 + element->len; + u_char *buf = ngx_palloc(cf->pool, sizeof(u_char) * var_name_len); + + if (buf == NULL) + { + return NGX_CONF_ERROR; + } + else + { + ngx_sprintf(buf, "jwt_claim_%V", element); + ngx_str_t *var_name = ngx_palloc(cf->pool, sizeof(ngx_str_t)); + + if (var_name == NULL) + { + return NGX_CONF_ERROR; + } + else + { + var_name->data = buf; + var_name->len = var_name_len; + + // NGX_HTTP_VAR_CHANGEABLE simplifies the required logic by assuming a JWT claim will always be the same for a given request + ngx_http_variable_t *http_var = ngx_http_add_variable(cf, var_name, NGX_HTTP_VAR_CHANGEABLE); + + if (http_var == NULL) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to add variable %V", var_name); + + return NGX_CONF_ERROR; + } + else + { + http_var->get_handler = get_jwt_var_claim; + + // store the index of this new claim in the claims array as the "data" that will be passed to the getter + ngx_uint_t *claim_idx = ngx_palloc(cf->pool, sizeof(ngx_uint_t)); + + if (claim_idx == NULL) + { + return NGX_CONF_ERROR; + } + else + { + *claim_idx = claims->nelts - 1; + http_var->data = (uintptr_t) claim_idx; + } + } + } + } + } + + return NGX_CONF_OK; +} + +static ngx_int_t get_jwt_var_claim(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) +{ + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "getting jwt value for var index %l", *((ngx_uint_t*) data)); + auth_jwt_ctx_t *ctx = get_request_jwt_ctx(r); + + if (ctx == NULL) + { + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "no module context found while getting jwt value"); + + return NGX_ERROR; + } + else + { + ngx_uint_t *claim_idx = (ngx_uint_t*) data; + ngx_str_t claim_value = ((ngx_str_t*) ctx->claim_values->elts)[*claim_idx]; + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->len = claim_value.len; + v->data = claim_value.data; + + return NGX_OK; + } +} + static char *merge_extract_claims(ngx_conf_t *cf, ngx_array_t *claims) { ngx_str_t *values = cf->args->elts; @@ -295,98 +417,169 @@ static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, v return merge_extract_claims(cf, claims); } -static ngx_int_t handle_request(ngx_http_request_t *r) +static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) +{ + auth_jwt_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_auth_jwt_module); + + if (ctx != NULL) + { + return ctx; + } + else + { + ctx = ngx_pcalloc(r->pool, sizeof(auth_jwt_ctx_t)); + + if (ctx == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error allocating jwt module context"); + return ctx; + } + else { + if (jwtcf->extract_var_claims != NULL) + { + ctx->claim_values = ngx_array_create(r->pool, jwtcf->extract_var_claims->nelts, sizeof(ngx_str_t)); + + if (ctx->claim_values == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error initializing jwt module context"); + return NULL; + } + } + + ctx->validation_status = NGX_AGAIN; + ngx_http_set_ctx(r, ctx, ngx_http_auth_jwt_module); + + return ctx; + } + } +} + +// this creates the module's context struct and extracts claim vars the first time it is called, +// either from the access-phase handler or an http var getter +static auth_jwt_ctx_t *get_request_jwt_ctx(ngx_http_request_t *r) { auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); - if (!jwtcf->enabled) + if(!jwtcf->enabled) { - return NGX_DECLINED; + return NULL; + } + + auth_jwt_ctx_t *ctx = get_or_init_jwt_module_ctx(r, jwtcf); + + if (ctx == NULL) + { + return NULL; + } + else if (ctx->validation_status != NGX_AGAIN) + { + // we already validated and extacted everything we care about, so we just return the already-complete context + return ctx; + } + + char *jwtPtr = get_jwt(r, jwtcf->jwt_location); + + if (jwtPtr == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a JWT"); + ctx->validation_status = NGX_ERROR; + return ctx; } else { - // pass through options requests without token authentication - if (r->method == NGX_HTTP_OPTIONS) + ngx_str_t algorithm = jwtcf->algorithm; + int keyLength; + u_char *key; + jwt_t *jwt = NULL; + + if (algorithm.len == 0 || (algorithm.len == 5 && ngx_strncmp(algorithm.data, "HS", 2) == 0)) { - return NGX_DECLINED; + keyLength = jwtcf->key.len / 2; + key = ngx_palloc(r->pool, keyLength); + + if (0 != hex_to_binary((char *)jwtcf->key.data, key, jwtcf->key.len)) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); + ctx->validation_status = NGX_ERROR; + return ctx; + } } - else + else if (algorithm.len == 5 && (ngx_strncmp(algorithm.data, "RS", 2) == 0 || ngx_strncmp(algorithm.data, "ES", 2) == 0)) { - char *jwtPtr = get_jwt(r, jwtcf->jwt_location); - - if (jwtPtr == NULL) + if (jwtcf->use_keyfile == 1) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a JWT"); - return redirect(r, jwtcf); + keyLength = jwtcf->_keyfile.len; + key = (u_char *)jwtcf->_keyfile.data; } else { - ngx_str_t algorithm = jwtcf->algorithm; - int keyLength; - u_char *key; - jwt_t *jwt = NULL; - - if (algorithm.len == 0 || (algorithm.len == 5 && ngx_strncmp(algorithm.data, "HS", 2) == 0)) - { - keyLength = jwtcf->key.len / 2; - key = ngx_palloc(r->pool, keyLength); + keyLength = jwtcf->key.len; + key = jwtcf->key.data; + } + } + else + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm %s", algorithm); + ctx->validation_status = NGX_ERROR; + return ctx; + } - if (0 != hex_to_binary((char *)jwtcf->key.data, key, jwtcf->key.len)) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); - return redirect(r, jwtcf); - } - } - else if (algorithm.len == 5 && (ngx_strncmp(algorithm.data, "RS", 2) == 0 || ngx_strncmp(algorithm.data, "ES", 2) == 0)) - { - if (jwtcf->use_keyfile == 1) - { - keyLength = jwtcf->_keyfile.len; - key = (u_char *)jwtcf->_keyfile.data; - } - else - { - keyLength = jwtcf->key.len; - key = jwtcf->key.data; - } - } - else - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm %s", algorithm); - return redirect(r, jwtcf); - } + if (jwt_decode(&jwt, jwtPtr, key, keyLength) != 0 || !jwt) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT"); + ctx->validation_status = NGX_ERROR; + } + else if (validate_alg(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm specified"); + ctx->validation_status = NGX_ERROR; + } + else if (validate_exp(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); + ctx->validation_status = NGX_ERROR; + } + else if (validate_sub(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); + ctx->validation_status = NGX_ERROR; + } + else + { + extract_request_claims(r, jwtcf, jwt); + extract_response_claims(r, jwtcf, jwt); + ctx->validation_status = extract_var_claims(r, jwtcf, jwt, ctx); + } - if (jwt_decode(&jwt, jwtPtr, key, keyLength) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT"); - return redirect(r, jwtcf); - } + jwt_free(jwt); + return ctx; + } +} - if (validate_alg(jwtcf, jwt) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm specified"); - return free_jwt_and_redirect(r, jwtcf, jwt); - } - else if (validate_exp(jwtcf, jwt) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); - return free_jwt_and_redirect(r, jwtcf, jwt); - } - else if (validate_sub(jwtcf, jwt) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); - return free_jwt_and_redirect(r, jwtcf, jwt); - } - else - { - extract_request_claims(r, jwtcf, jwt); - extract_response_claims(r, jwtcf, jwt); - jwt_free(jwt); +static ngx_int_t handle_request(ngx_http_request_t *r) +{ + auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); + auth_jwt_ctx_t *ctx = get_request_jwt_ctx(r); - return NGX_OK; - } - } - } + if (!jwtcf->enabled) + { + return NGX_DECLINED; + } + else if (r->method == NGX_HTTP_OPTIONS) // pass through options requests without token authentication + { + return NGX_DECLINED; + } + else if (!ctx) + { + return NGX_ERROR; + } + else if (ctx->validation_status == NGX_ERROR) + { + return redirect(r, jwtcf); + } + else + { + return ctx->validation_status; } } @@ -394,8 +587,7 @@ static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { const jwt_alg_t alg = jwt_get_alg(jwt); - if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512 && alg != JWT_ALG_ES256 && alg != JWT_ALG_ES384 && alg != JWT_ALG_ES512) - { + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512 && alg != JWT_ALG_ES256 && alg != JWT_ALG_ES384 && alg != JWT_ALG_ES512) { return 1; } @@ -430,6 +622,37 @@ static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt) return 0; } +static ngx_int_t extract_var_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt, auth_jwt_ctx_t *ctx) +{ + ngx_array_t *claims = jwtcf->extract_var_claims; + + if (claims == NULL || claims->nelts == 0) + { + return NGX_OK; + } + else + { + const ngx_str_t *claimsPtr = claims->elts; + + for (uint i = 0; i < claims->nelts; ++i) + { + const ngx_str_t claim = claimsPtr[i]; + const char *claimValue = jwt_get_grant(jwt, (char *)claim.data); + ngx_str_t value = ngx_string(""); + + if (claimValue != NULL && strlen(claimValue) > 0) + { + value = char_ptr_to_ngx_str_t(r->pool, claimValue); + } + + ((ngx_str_t*) ctx->claim_values->elts)[i] = value; + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "set var %V to JWT claim value %s", &claim, value.data); + } + + return NGX_OK; + } +} + static void extract_claims(ngx_http_request_t *r, jwt_t *jwt, ngx_array_t *claims, ngx_int_t (*set_header)(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value)) { if (claims != NULL && claims->nelts > 0) @@ -467,16 +690,6 @@ static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtc extract_claims(r, jwt, jwtcf->extract_response_claims, set_response_header); } -static ngx_int_t free_jwt_and_redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) -{ - if (jwt) - { - jwt_free(jwt); - } - - return redirect(r, jwtcf); -} - static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) { if (jwtcf->redirect) diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 229d545..5359434 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -395,4 +395,48 @@ vXjq39xtcIBRTO1c2zs= try_files index.html =404; } } + + location /secure/extract-claim/if/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_var_claims sub; + + if ($jwt_claim_sub = 'some-long-uuid') { + return 200; + } + return 401; + } + + location /secure/extract-claim/body/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_var_claims sub; + + return 200 "sub: $jwt_claim_sub"; + } + + location /secure/extract-claim/body/multiple { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_validate_sub on; + auth_jwt_extract_var_claims firstName middleName lastName; + + return 200 "you are: $jwt_claim_firstName $jwt_claim_middleName $jwt_claim_lastName"; + } + + location /profile { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_validate_sub on; + + location /profile/me { + auth_jwt_extract_var_claims sub; + + return 301 /profile/$jwt_claim_sub; + } + } } diff --git a/test/test.sh b/test/test.sh index 2bf9cb3..747124c 100755 --- a/test/test.sh +++ b/test/test.sh @@ -72,7 +72,7 @@ run_test () { fi fi - if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ "${expectedResponseRegex}" ]]; then + if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ ${expectedResponseRegex} ]]; then printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex}" NUM_FAILED=$((${NUM_FAILED} + 1)) okay=0 @@ -279,7 +279,7 @@ main() { run_test -n 'extracts nested claim to request variable' \ -p '/secure/extract-claim/request/nested' \ - -r '< Test: username=hello.world' \ + -r '< Test: username=hello\.world' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' run_test -n 'extracts single claim to response variable' \ @@ -319,7 +319,35 @@ main() { run_test -n 'extracts nested claim to response header' \ -p '/secure/extract-claim/response/nested' \ - -r '< JWT-username: hello.world' \ + -r '< JWT-username: hello\.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'tests single claim with if statement' \ + -p '/secure/extract-claim/if/sub' \ + -c 200 \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'tests absence of single claim with if statement' \ + -p '/secure/extract-claim/if/sub' \ + -c 401 \ + -x '--header "Authorization: Bearer ${JWT_HS256_MISSING_SUB}"' + + run_test -n 'extracts single claim to response body' \ + -p '/secure/extract-claim/body/sub' \ + -c 200 \ + -r 'sub: some-long-uuid$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims to response body' \ + -p '/secure/extract-claim/body/multiple' \ + -c 200 \ + -r 'you are: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'redirect based on claim' \ + -p '/profile/me' \ + -c 301 \ + -r '< Location: http://nginx:8000/profile/some-long-uuid' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' if [[ "${NUM_FAILED}" = '0' ]]; then From 5c3d1b9565edb5ac1243d3ebbaa869246f82f0f6 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 4 Feb 2025 12:35:50 -0500 Subject: [PATCH 119/130] fix workflow --- .github/workflows/make-releases.yml | 56 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 7316e6b..3e58bfa 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -1,11 +1,6 @@ -name: CI +name: Make Releases on: - push: - branches: - - master - paths: - - src/** workflow_dispatch: jobs: @@ -27,14 +22,26 @@ jobs: libjwt-version: ['1.12.0', '1.14.0', '1.15.3'] runs-on: ubuntu-latest steps: + - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - path: 'ngx-http-auth-jwt-module' + fetch-depth: 0 + path: ngx-http-auth-jwt-module + + - name: Get Metadata + id: meta + run: | + set -eux + cd ngx-http-auth-jwt-module + + tag=$(git describe --tags --abbrev=0) + + echo "filename=ngx-http-auth-jwt-module-${tag}_libjwt-${{matrix.libjwt-version}}_nginx-${{matrix.nginx-version}}.tgz" >> $GITHUB_OUTPUT # TODO cache the build result so we don't have to do this every time? - name: Download jansson - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'akheron/jansson' ref: 'v2.14' @@ -50,7 +57,7 @@ jobs: # TODO cache the build result so we don't have to do this every time? - name: Download libjwt - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'benmcollins/libjwt' ref: 'v${{matrix.libjwt-version}}' @@ -82,44 +89,43 @@ jobs: BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" fi - ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} + ./configure --with-compat --without-http_rewrite_module --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} - name: Make Modules working-directory: ./nginx run: make modules - - name: Create release archive + - name: Create Release Archive run: | cp ./nginx/objs/ngx_http_auth_jwt_module.so ./ - tar czf ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz ngx_http_auth_jwt_module.so + tar czf ${{steps.meta.outputs.filename}} ngx_http_auth_jwt_module.so - - name: Upload build artifact + - name: Upload Build Artifact uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz - path: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz + name: ${{steps.meta.outputs.filename}} + path: ${{steps.meta.outputs.filename}} update_releases_page: - name: Upload builds to Releases - if: github.event_name != 'pull_request' - needs: - - build + name: Upload Release + needs: build runs-on: ubuntu-latest permissions: contents: write steps: - - name: Set up variables + + - name: Set-up Variables id: vars run: | echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" - - name: Download build artifacts from previous jobs - uses: actions/download-artifact@v3 + - name: Download Build Artifacts from Previous Jobs + uses: actions/download-artifact@v4 with: path: artifacts - - name: Upload builds to Releases + - name: Upload Builds to Release uses: ncipollo/release-action@v1 with: allowUpdates: true @@ -128,7 +134,7 @@ jobs: body: | > [!WARNING] > This is an automatically generated pre-release version of the module, which includes the latest master branch changes. - > Please report any bugs you find to the issue tracker. + > Please report any bugs you find. - Build Date: `${{ steps.vars.outputs.date_now }}` - Commit: ${{ github.sha }} From c38ae696319e5106a6c171d4f6882d1ffffd58db Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 5 Feb 2025 14:23:23 -0500 Subject: [PATCH 120/130] fix release artifact upload --- .github/workflows/make-releases.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 3e58bfa..8d7c906 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -105,10 +105,9 @@ jobs: with: if-no-files-found: error name: ${{steps.meta.outputs.filename}} - path: ${{steps.meta.outputs.filename}} - update_releases_page: - name: Upload Release + release: + name: Create/Update Release needs: build runs-on: ubuntu-latest permissions: @@ -120,7 +119,7 @@ jobs: run: | echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" - - name: Download Build Artifacts from Previous Jobs + - name: Download Build Artifacts uses: actions/download-artifact@v4 with: path: artifacts @@ -128,9 +127,7 @@ jobs: - name: Upload Builds to Release uses: ncipollo/release-action@v1 with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: artifacts/*/* + name: 'Development Build: ${{ github.ref_name }}@${{ github.sha }}' body: | > [!WARNING] > This is an automatically generated pre-release version of the module, which includes the latest master branch changes. @@ -138,7 +135,9 @@ jobs: - Build Date: `${{ steps.vars.outputs.date_now }}` - Commit: ${{ github.sha }} - name: 'Development build: ${{ github.ref_name }}@${{ github.sha }}' prerelease: true + allowUpdates: true removeArtifacts: true + artifactErrorsFailBuild: true + artifacts: artifacts/* tag: dev-build From acbb12e3c83d9a1d33e673407530cc7bcc8031e6 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 5 Feb 2025 14:23:33 -0500 Subject: [PATCH 121/130] update redirect URL to include port (#146) --- README.md | 41 ++++++----- scripts | 17 +++-- src/ngx_http_auth_jwt_module.c | 119 +++++++++++++++++++++----------- test/docker-compose-test.yml | 7 +- test/etc/nginx/conf.d/test.conf | 6 ++ test/test-nginx.dockerfile | 11 +-- test/test-runner.dockerfile | 14 ++-- test/test.sh | 28 ++++++-- 8 files changed, 159 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 95c83af..f6b09d6 100644 --- a/README.md +++ b/README.md @@ -192,41 +192,43 @@ Once you save your changes to `.vscode/c_cpp_properties.json`, you should see th ### Building and Testing -The `./scripts.sh` file contains multiple commands to make things easy: +The `./scripts` file contains multiple commands to make things easy: | Command | Description | | --------------------- | ----------------------------------------------------------------- | | `build_module` | Builds the NGINX image. | | `rebuild_module` | Re-builds the NGINX image. | -| `start_nginx` | Starts the NGINX container. | -| `stop_nginx` | Stops the NGINX container. | +| `start` | Starts the NGINX container. | +| `stop` | Stops the NGINX container. | | `cp_bin` | Copies the compiled binaries out of the NGINX container. | -| `build_test_runner` | Builds the images used by the test stack (uses Docker compose). | -| `rebuild_test_runner` | Re-builds the images used by the test stack. | -| `test` | Runs `test.sh` against the NGINX container (uses Docker compose). | +| `build_test` | Builds the images used by the test stack. | +| `rebuild_test` | Re-builds the images used by the test stack. | +| `test` | Runs `test.sh` against the NGINX container. | | `test_now` | Runs `test.sh` without rebuilding. | You can run multiple commands in sequence by separating them with a space, e.g.: ```shell -./scripts.sh build_module test +./scripts build_module +./scripts test ``` -To build the Docker images, module, start NGINX, and run the tests against, you can simply do: +To build the Docker images, module, start NGINX, and run the tests against it for all versions, you can simply do: ```shell -./scripts.sh all +./scripts all ``` -When you make a change to the module run `./scripts.sh build_module test` to build a fresh module and run the tests. Note that `rebuild_module` is not often needed as `build_module` hashes the module's source files which will cause a cache miss while building the container, causing the module to be rebuilt. +When you make a change to the module, running `./scripts test` should build a fresh module and run the tests. Note that `rebuild_module` is not often needed as Docker will automatically rebuild the image if the source files have +changed. -When you make a change to the test NGINX config or `test.sh`, run `./scripts.sh test` to run the tests. Similar to above, the test sources are hashed and the containers will be rebuilt as needed. +When you make a change to the test NGINX config or `test.sh`, run `./scripts test` to run the tests. -The image produced with `./scripts.sh build_module` only differs from the official NGINX image in two ways: +The image produced with `./scripts build_module` only differs from the official NGINX image in two ways: - the JWT module itself, and - the `nginx.conf` file is overwritten with our own. -The tests use a customized NGINX image, distinct from the main image, as well as a test runner image. By running `./scripts.sh test`, the two test containers will be stood up via Docker compose, then they'll be started, and the tests will run. At the end of the test run, both containers will be automatically stopped and destroyed. See below to learn how to trace test failures across runs. +The tests use a customized NGINX image, distinct from the main image, as well as a test runner image. By running `./scripts test`, the two test containers will be stood up via Docker Compose, then they'll be started, and the tests will run. At the end of the test run, both containers will be automatically stopped and destroyed. See below to learn how to trace test failures across runs. #### Tracing Test Failures @@ -236,20 +238,23 @@ If you'd like to persist logs across test runs, you can configure the log driver ```shell # need to rebuild the test runner with the proper log driver -LOG_DRIVER=journald ./scripts.sh rebuild_test_runner +export LOG_DRIVER=journald + +# rebuild the test images +./scripts rebuild_test # run the tests -./scripts.sh test +./scripts test -# check the logs -journalctl -eu docker CONTAINER_NAME=jwt-nginx-test +# check the logs -- adjust the container name as needed +journalctl -eu docker CONTAINER_NAME=nginx-auth-jwt-test-nginx ``` Now you'll be able to see logs from previous test runs. The best way to make use of this is to open two terminals, one where you run the tests, and one where you follow the logs: ```shell # terminal 1 -./scripts.sh test +./scripts test # terminal 2 journalctl -fu docker CONTAINER_NAME=jwt-nginx-test diff --git a/scripts b/scripts index 9d90185..59fc5c3 100755 --- a/scripts +++ b/scripts @@ -31,6 +31,7 @@ IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} FULL_IMAGE_NAME=${ORG_NAME:-teslagov}/${IMAGE_NAME} TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" +TEST_COMPOSE_FILE='test/docker-compose-test.yml' all() { build_module @@ -162,7 +163,8 @@ build_test() { printf "${MAGENTA}Building test NGINX & runner using port ${port}...${NC}\n" docker compose \ -p ${TEST_CONTAINER_NAME_PREFIX} \ - -f ./test/docker-compose-test.yml build \ + -f ${TEST_COMPOSE_FILE} \ + build \ --build-arg RUNNER_BASE_IMAGE=${runnerBaseImage} \ --build-arg PORT=${port} \ --build-arg SSL_PORT=${sslPort} \ @@ -188,12 +190,11 @@ test() { printf "${MAGENTA}Running tests...${NC}\n" docker compose \ -p ${TEST_CONTAINER_NAME_PREFIX} \ - -f ./test/docker-compose-test.yml up \ + -f ${TEST_COMPOSE_FILE} up \ --no-start - - trap 'docker compose -f ./test/docker-compose-test.yml down' 0 - + trap test_cleanup 0 + test_now } @@ -218,6 +219,12 @@ test_now() { echo " NGINX Version: ${NGINX_VERSION}" } +test_cleanup() { + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} down +} + get_port() { startPort=${1:-8000} endPort=$((startPort + 100)) diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index fe428b4..59b84ac 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -290,13 +290,13 @@ static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void * { // add this claim's name to the config struct ngx_str_t *element = ngx_array_push(claims); - + *element = values[i]; // add an http variable for this claim size_t var_name_len = 10 + element->len; u_char *buf = ngx_palloc(cf->pool, sizeof(u_char) * var_name_len); - + if (buf == NULL) { return NGX_CONF_ERROR; @@ -305,7 +305,7 @@ static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void * { ngx_sprintf(buf, "jwt_claim_%V", element); ngx_str_t *var_name = ngx_palloc(cf->pool, sizeof(ngx_str_t)); - + if (var_name == NULL) { return NGX_CONF_ERROR; @@ -314,31 +314,31 @@ static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void * { var_name->data = buf; var_name->len = var_name_len; - + // NGX_HTTP_VAR_CHANGEABLE simplifies the required logic by assuming a JWT claim will always be the same for a given request ngx_http_variable_t *http_var = ngx_http_add_variable(cf, var_name, NGX_HTTP_VAR_CHANGEABLE); - + if (http_var == NULL) { ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to add variable %V", var_name); - + return NGX_CONF_ERROR; } else { http_var->get_handler = get_jwt_var_claim; - + // store the index of this new claim in the claims array as the "data" that will be passed to the getter ngx_uint_t *claim_idx = ngx_palloc(cf->pool, sizeof(ngx_uint_t)); - + if (claim_idx == NULL) { - return NGX_CONF_ERROR; + return NGX_CONF_ERROR; } else { *claim_idx = claims->nelts - 1; - http_var->data = (uintptr_t) claim_idx; + http_var->data = (uintptr_t)claim_idx; } } } @@ -350,26 +350,26 @@ static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void * static ngx_int_t get_jwt_var_claim(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { - ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "getting jwt value for var index %l", *((ngx_uint_t*) data)); + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "getting jwt value for var index %l", *((ngx_uint_t *)data)); auth_jwt_ctx_t *ctx = get_request_jwt_ctx(r); - + if (ctx == NULL) { ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "no module context found while getting jwt value"); - + return NGX_ERROR; } else { - ngx_uint_t *claim_idx = (ngx_uint_t*) data; - ngx_str_t claim_value = ((ngx_str_t*) ctx->claim_values->elts)[*claim_idx]; - + ngx_uint_t *claim_idx = (ngx_uint_t *)data; + ngx_str_t claim_value = ((ngx_str_t *)ctx->claim_values->elts)[*claim_idx]; + v->valid = 1; v->no_cacheable = 0; v->not_found = 0; v->len = claim_value.len; v->data = claim_value.data; - + return NGX_OK; } } @@ -420,7 +420,7 @@ static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, v static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) { auth_jwt_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_auth_jwt_module); - + if (ctx != NULL) { return ctx; @@ -434,7 +434,8 @@ static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jw ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error allocating jwt module context"); return ctx; } - else { + else + { if (jwtcf->extract_var_claims != NULL) { ctx->claim_values = ngx_array_create(r->pool, jwtcf->extract_var_claims->nelts, sizeof(ngx_str_t)); @@ -460,7 +461,7 @@ static auth_jwt_ctx_t *get_request_jwt_ctx(ngx_http_request_t *r) { auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); - if(!jwtcf->enabled) + if (!jwtcf->enabled) { return NULL; } @@ -587,7 +588,8 @@ static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { const jwt_alg_t alg = jwt_get_alg(jwt); - if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512 && alg != JWT_ALG_ES256 && alg != JWT_ALG_ES384 && alg != JWT_ALG_ES512) { + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512 && alg != JWT_ALG_ES256 && alg != JWT_ALG_ES384 && alg != JWT_ALG_ES512) + { return 1; } @@ -625,7 +627,7 @@ static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt) static ngx_int_t extract_var_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt, auth_jwt_ctx_t *ctx) { ngx_array_t *claims = jwtcf->extract_var_claims; - + if (claims == NULL || claims->nelts == 0) { return NGX_OK; @@ -633,22 +635,22 @@ static ngx_int_t extract_var_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtc else { const ngx_str_t *claimsPtr = claims->elts; - + for (uint i = 0; i < claims->nelts; ++i) { const ngx_str_t claim = claimsPtr[i]; const char *claimValue = jwt_get_grant(jwt, (char *)claim.data); ngx_str_t value = ngx_string(""); - + if (claimValue != NULL && strlen(claimValue) > 0) { value = char_ptr_to_ngx_str_t(r->pool, claimValue); } - - ((ngx_str_t*) ctx->claim_values->elts)[i] = value; + + ((ngx_str_t *)ctx->claim_values->elts)[i] = value; ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "set var %V to JWT claim value %s", &claim, value.data); } - + return NGX_OK; } } @@ -708,11 +710,16 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) if (r->method == NGX_HTTP_GET) { const int loginlen = jwtcf->loginurl.len; - const char *scheme = (r->connection->ssl) ? "https" : "http"; + const char *scheme = r->connection->ssl ? "https" : "http"; + ngx_str_t port_variable_name = ngx_string("server_port"); + ngx_int_t port_variable_hash = ngx_hash_key(port_variable_name.data, port_variable_name.len); + ngx_http_variable_value_t *port_var = ngx_http_get_variable(r, &port_variable_name, port_variable_hash); + char *port_str = ""; + uint port_str_len = 0; const ngx_str_t server = r->headers_in.server; ngx_str_t uri_variable_name = ngx_string("request_uri"); ngx_int_t uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); - ngx_http_variable_value_t *request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); + ngx_http_variable_value_t *uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); ngx_str_t uri; ngx_str_t uri_escaped; uintptr_t escaped_len; @@ -720,12 +727,12 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) int return_url_idx; // get the URI - if (request_uri_var && !request_uri_var->not_found && request_uri_var->valid) + if (uri_var && !uri_var->not_found && uri_var->valid) { // ideally we would like the URI with the querystring parameters - uri.data = ngx_palloc(r->pool, request_uri_var->len); - uri.len = request_uri_var->len; - ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); + uri.data = ngx_palloc(r->pool, uri_var->len); + uri.len = uri_var->len; + ngx_memcpy(uri.data, uri_var->data, uri_var->len); } else { @@ -733,31 +740,59 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) uri = r->uri; } + if (port_var && !port_var->not_found && port_var->valid) + { + const ngx_uint_t port_num = ngx_atoi(port_var->data, port_var->len); + const bool is_default_port_80 = !r->connection->ssl && port_num == 80; + const bool is_default_port_443 = r->connection->ssl && port_num == 443; + const bool is_non_default_port = !is_default_port_80 && !is_default_port_443; + + if (is_non_default_port) + { + port_str = ngx_palloc(r->pool, NGX_INT_T_LEN + 2); + + ngx_snprintf((u_char *)port_str, sizeof(port_str), ":%d", port_num); + port_str_len = strlen(port_str); + } + } + + // escape the URI escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; uri_escaped.data = ngx_palloc(r->pool, escaped_len); uri_escaped.len = escaped_len; ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); - r->headers_out.location->value.len = loginlen + strlen("?return_url=") + strlen(scheme) + strlen("://") + server.len + uri_escaped.len; + // Add up the lengths of: login URL, "?return_url=", scheme, "://", server, port, uri (path) + r->headers_out.location->value.len = loginlen + 12 + strlen(scheme) + 3 + server.len + port_str_len + uri_escaped.len; return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); - ngx_memcpy(return_url, jwtcf->loginurl.data, jwtcf->loginurl.len); + ngx_memcpy(return_url, jwtcf->loginurl.data, jwtcf->loginurl.len); return_url_idx = jwtcf->loginurl.len; - ngx_memcpy(return_url + return_url_idx, "?return_url=", strlen("?return_url=")); - return_url_idx += strlen("?return_url="); - ngx_memcpy(return_url + return_url_idx, scheme, strlen(scheme)); + ngx_memcpy(return_url + return_url_idx, "?return_url=", 12); + return_url_idx += 12; + ngx_memcpy(return_url + return_url_idx, scheme, strlen(scheme)); return_url_idx += strlen(scheme); - ngx_memcpy(return_url + return_url_idx, "://", strlen("://")); - return_url_idx += strlen("://"); - ngx_memcpy(return_url + return_url_idx, server.data, server.len); + ngx_memcpy(return_url + return_url_idx, "://", 3); + return_url_idx += 3; + ngx_memcpy(return_url + return_url_idx, server.data, server.len); return_url_idx += server.len; - ngx_memcpy(return_url + return_url_idx, uri_escaped.data, uri_escaped.len); + + if (port_str_len > 0) + { + ngx_memcpy(return_url + return_url_idx, port_str, port_str_len); + return_url_idx += port_str_len; + } + + if (uri_escaped.len > 0) + { + ngx_memcpy(return_url + return_url_idx, uri_escaped.data, uri_escaped.len); + } r->headers_out.location->value.data = (u_char *)return_url; } diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index 14c88da..72ff710 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -1,20 +1,19 @@ services: nginx: - container_name: ${TEST_CONTAINER_NAME_PREFIX}-nginx + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-nginx build: context: . dockerfile: test-nginx.dockerfile args: - BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION} + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} logging: driver: ${LOG_DRIVER:-journald} runner: - container_name: ${TEST_CONTAINER_NAME_PREFIX}-runner + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-runner build: context: . dockerfile: test-runner.dockerfile - depends_on: - nginx \ No newline at end of file diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 5359434..4e5d764 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -405,6 +405,7 @@ vXjq39xtcIBRTO1c2zs= if ($jwt_claim_sub = 'some-long-uuid') { return 200; } + return 401; } @@ -439,4 +440,9 @@ vXjq39xtcIBRTO1c2zs= return 301 /profile/$jwt_claim_sub; } } + + location /return-url { + auth_jwt_enabled on; + auth_jwt_redirect on; + } } diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile index e12acb4..1065558 100644 --- a/test/test-nginx.dockerfile +++ b/test/test-nginx.dockerfile @@ -1,13 +1,11 @@ ARG BASE_IMAGE -ARG PORT -ARG SSL_PORT -FROM ${BASE_IMAGE} AS NGINX +FROM ${BASE_IMAGE:?required} AS NGINX ARG PORT ARG SSL_PORT + COPY etc/ /etc/ -RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf -RUN sed -i "s|%{SSL_PORT}|${SSL_PORT}|" /etc/nginx/conf.d/test.conf + COPY <<` /usr/share/nginx/html/index.html Test @@ -16,3 +14,6 @@ COPY <<` /usr/share/nginx/html/index.html ` + +RUN sed -i "s|%{PORT}|${PORT:?required}|" /etc/nginx/conf.d/test.conf +RUN sed -i "s|%{SSL_PORT}|${SSL_PORT:?required}|" /etc/nginx/conf.d/test.conf diff --git a/test/test-runner.dockerfile b/test/test-runner.dockerfile index 0aca095..18fc3d3 100644 --- a/test/test-runner.dockerfile +++ b/test/test-runner.dockerfile @@ -1,16 +1,18 @@ ARG RUNNER_BASE_IMAGE -ARG PORT -ARG SSL_PORT -FROM ${RUNNER_BASE_IMAGE} +FROM ${RUNNER_BASE_IMAGE:?required} ARG PORT ARG SSL_PORT -ENV PORT=${PORT} -ENV SSL_PORT=${SSL_PORT} + +ENV PORT=${PORT:?required} +ENV SSL_PORT=${SSL_PORT:?required} + RUN <<` set -e apt-get update apt-get install -y curl bash ` + COPY test.sh . -CMD ./test.sh ${PORT} ${SSL_PORT} + +CMD ./test.sh diff --git a/test/test.sh b/test/test.sh index 747124c..c726a75 100755 --- a/test/test.sh +++ b/test/test.sh @@ -29,7 +29,7 @@ run_test () { local response= local testNum="${GRAY}${NUM_TESTS}${NC}\t" - while getopts "n:sp:r:c:x:" option; do + while getopts "n:asp:r:c:x:" option; do case $option in n) name=$OPTARG;; @@ -73,7 +73,7 @@ run_test () { fi if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ ${expectedResponseRegex} ]]; then - printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex}" + printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex//%/%%}" NUM_FAILED=$((${NUM_FAILED} + 1)) okay=0 fi @@ -113,8 +113,8 @@ main() { -p '/' \ -c '200' - run_test -s \ - -n '[SSL] when auth disabled, should return 200' \ + run_test -n '[SSL] when auth disabled, should return 200' \ + -s \ -p '/' \ -c '200' @@ -350,6 +350,26 @@ main() { -r '< Location: http://nginx:8000/profile/some-long-uuid' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + run_test -n 'returns 302 if auth enabled and no JWT provided' \ + -p '/return-url' \ + -c '302' + + run_test -n 'redirects to login if auth enabled and no JWT provided' \ + -p '/return-url' \ + -r '< Location: https://example\.com/login.*' + + run_test -n 'adds return_url to login URL when redirected to login' \ + -p '/return-url' \ + -r '< Location: https://example\.com/login\?return_url=http://nginx.*' + + run_test -n 'return_url includes port when redirected to login' \ + -p '/return-url' \ + -r "< Location: https://example\.com/login\?return_url=http://nginx:${PORT}/return-url" + + run_test -n 'return_url includes query when redirected to login' \ + -p '/return-url?test=123' \ + -r '< Location: https://example\.com/login\?return_url=http://nginx.*/return-url%3Ftest=123' + if [[ "${NUM_FAILED}" = '0' ]]; then printf "\nRan ${NUM_TESTS} tests successfully (skipped ${NUM_SKIPPED}).\n" return 0 From a2e3e914998d4b82e91d2db9ec9e482f3b562c43 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 5 Feb 2025 14:37:41 -0500 Subject: [PATCH 122/130] fix artifact processing in workflow --- .github/workflows/make-releases.yml | 62 +++- test/docker-compose-test.yml | 19 -- test/ec_key_256.pem | 5 - test/ec_key_384.pem | 6 - test/ec_key_521.pem | 8 - test/etc/nginx/conf.d/test.conf | 448 ---------------------------- test/etc/nginx/ec_key_256-pub.pem | 4 - test/etc/nginx/ec_key_384-pub.pem | 5 - test/etc/nginx/ec_key_521-pub.pem | 6 - test/etc/nginx/rsa_key_2048-pub.pem | 9 - test/etc/nginx/test.crt | 23 -- test/etc/nginx/test.key | 28 -- test/rsa_key_2048.pem | 28 -- test/test-nginx.dockerfile | 19 -- test/test-runner.dockerfile | 18 -- test/test.sh | 387 ------------------------ 16 files changed, 49 insertions(+), 1026 deletions(-) delete mode 100644 test/docker-compose-test.yml delete mode 100644 test/ec_key_256.pem delete mode 100644 test/ec_key_384.pem delete mode 100644 test/ec_key_521.pem delete mode 100644 test/etc/nginx/conf.d/test.conf delete mode 100644 test/etc/nginx/ec_key_256-pub.pem delete mode 100644 test/etc/nginx/ec_key_384-pub.pem delete mode 100644 test/etc/nginx/ec_key_521-pub.pem delete mode 100755 test/etc/nginx/rsa_key_2048-pub.pem delete mode 100644 test/etc/nginx/test.crt delete mode 100644 test/etc/nginx/test.key delete mode 100755 test/rsa_key_2048.pem delete mode 100644 test/test-nginx.dockerfile delete mode 100644 test/test-runner.dockerfile delete mode 100755 test/test.sh diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 8d7c906..487c4bf 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -4,8 +4,29 @@ on: workflow_dispatch: jobs: + meta: + name: Get Metadata + runs-on: ubuntu-latest + outputs: + tag: ${{steps.meta.outputs.tag}} + steps: + + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get Metadata + id: meta + run: | + set -eu + tag=$(git describe --tags --abbrev=0) + + echo "tag=${tag}" >> $GITHUB_OUTPUT + build: name: "NGINX: ${{ matrix.nginx-version }}; libjwt: ${{ matrix.libjwt-version }}" + needs: meta strategy: matrix: # NGINX versions to build/test against @@ -26,18 +47,17 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 with: - fetch-depth: 0 path: ngx-http-auth-jwt-module - name: Get Metadata id: meta run: | - set -eux - cd ngx-http-auth-jwt-module - - tag=$(git describe --tags --abbrev=0) + set -eu + artifact="ngx-http-auth-jwt-module-${{needs.meta.outputs.tag}}_libjwt-${{matrix.libjwt-version}}_nginx-${{matrix.nginx-version}}" + + echo "artifact=${artifact}" >> $GITHUB_OUTPUT + echo "filename=${artifact}.tgz" >> $GITHUB_OUTPUT - echo "filename=ngx-http-auth-jwt-module-${tag}_libjwt-${{matrix.libjwt-version}}_nginx-${{matrix.nginx-version}}.tgz" >> $GITHUB_OUTPUT # TODO cache the build result so we don't have to do this every time? - name: Download jansson @@ -104,11 +124,14 @@ jobs: uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: ${{steps.meta.outputs.filename}} + name: ${{steps.meta.outputs.artifact}} + path: ${{steps.meta.outputs.filename}} release: name: Create/Update Release - needs: build + needs: + - meta + - build runs-on: ubuntu-latest permissions: contents: write @@ -119,25 +142,38 @@ jobs: run: | echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" - - name: Download Build Artifacts + - name: Download Artifacts uses: actions/download-artifact@v4 with: path: artifacts - - name: Upload Builds to Release + - name: Flatten Artifacts + run: | + set -eu + + cd artifacts + + for f in $(find . -type f); do + echo "Staging: ${f}" + mv "${f}" . + done + + find . -type d -mindepth 1 -exec rm -rf "{}" + + + - name: Create/Update Release uses: ncipollo/release-action@v1 with: - name: 'Development Build: ${{ github.ref_name }}@${{ github.sha }}' + tag: ${{needs.meta.outputs.tag}} + name: "Pre-release: ${{needs.meta.outputs.tag}}" body: | > [!WARNING] > This is an automatically generated pre-release version of the module, which includes the latest master branch changes. > Please report any bugs you find. - Build Date: `${{ steps.vars.outputs.date_now }}` - - Commit: ${{ github.sha }} + - Commit: `${{ github.sha }}` prerelease: true allowUpdates: true removeArtifacts: true artifactErrorsFailBuild: true artifacts: artifacts/* - tag: dev-build diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml deleted file mode 100644 index 72ff710..0000000 --- a/test/docker-compose-test.yml +++ /dev/null @@ -1,19 +0,0 @@ -services: - - nginx: - container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-nginx - build: - context: . - dockerfile: test-nginx.dockerfile - args: - BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} - logging: - driver: ${LOG_DRIVER:-journald} - - runner: - container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-runner - build: - context: . - dockerfile: test-runner.dockerfile - depends_on: - - nginx \ No newline at end of file diff --git a/test/ec_key_256.pem b/test/ec_key_256.pem deleted file mode 100644 index 4206969..0000000 --- a/test/ec_key_256.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOlEBGcZxxhv8FkN0 -YIvax6fnhJbMeotzIEBxIglkNu6hRANCAATP1NpDzvZmKd2Mw6hIrv4nzUfNu7OK -mT5VuL5LhvUgzTqVGuxwevA7DlFsNVSfCljIBG3geio3fcd4k0Z9SygL ------END PRIVATE KEY----- diff --git a/test/ec_key_384.pem b/test/ec_key_384.pem deleted file mode 100644 index 2aa5780..0000000 --- a/test/ec_key_384.pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDADyrL6llSQoQOZ/PF/ -l761kAbrTwn4vu30Kr34ScW6bRKVXLq3cT3QssJ1nF9B63qhZANiAAQ48dOfIEd3 -0TCVE0JT4ZU0Db7Ftz+ex7lojP7uqTY9OI59yoMB01zUN4JK30BRXS9Yv0A9Bu1z -fgLu93FSn0kd0zIPMvuu5LUt60M/miSt2lA0OrqFhKjx6FFdN/lNh64= ------END PRIVATE KEY----- diff --git a/test/ec_key_521.pem b/test/ec_key_521.pem deleted file mode 100644 index 10471dc..0000000 --- a/test/ec_key_521.pem +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAKkag6aVn4XAbaALo -0b3pypdP5RBX7uKxHmKlkNCcpA0oVTdgjnM5NpJP8ZOM6NjVhEzsn6c/Tdn8hL8w -SI55hFWhgYkDgYYABABpTipSvbs8fq44u4fA+v7DTNYViA58sqbrxjxdzwWZ8eEj -CXsH7yzSGx3Y19NSyrX8HbjWmrj5uxiKeFCB8mGzTwDcFIKCMeMkHjZs/fmVOumR -a2XSpj7BP6wqcN6Pf+UqECivGAZGRHoabo/dm5zF9M3gO+G9eOrf3G1wgFFM7Vzb -Ow== ------END PRIVATE KEY----- diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf deleted file mode 100644 index 4e5d764..0000000 --- a/test/etc/nginx/conf.d/test.conf +++ /dev/null @@ -1,448 +0,0 @@ -error_log /var/log/nginx/debug.log debug; -access_log /var/log/nginx/access.log; - -server { - listen %{PORT}; - listen %{SSL_PORT} ssl; - server_name localhost; - - ssl_certificate /etc/nginx/test.crt; - ssl_certificate_key /etc/nginx/test.key; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - - auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; - auth_jwt_loginurl "https://example.com/login"; - auth_jwt_enabled off; - - location / { - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/default { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/default/validate-sub { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_validate_sub on; - auth_jwt_location COOKIE=jwt; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/default/no-redirect { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location COOKIE=jwt; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/hs256 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - auth_jwt_algorithm HS256; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/hs384 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - auth_jwt_algorithm HS384; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/hs512 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - auth_jwt_algorithm HS512; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/es256 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - auth_jwt_algorithm ES256; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz -ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/es384 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - auth_jwt_algorithm ES384; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 -aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 -LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/cookie/es512 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location COOKIE=jwt; - auth_jwt_algorithm ES512; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO -fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC -gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh -vXjq39xtcIBRTO1c2zs= ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/default { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/default/no-redirect { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/default/proxy-header { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - - add_header "Test-Authorization" "$http_authorization"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/rs256 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh -uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ -iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM -ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g -6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe -K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t -BwIDAQAB ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/es256 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz -ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/es384 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 -aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 -LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/es512 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO -fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC -gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh -vXjq39xtcIBRTO1c2zs= ------END PUBLIC KEY-----"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/rs256/file { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_algorithm RS256; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/rs384/file { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_algorithm RS384; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/rs512/file { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_algorithm RS512; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/es256/file { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_algorithm ES256; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/ec_key_256-pub.pem"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/es384/file { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_algorithm ES384; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/ec_key_384-pub.pem"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/auth-header/es512/file { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Authorization; - auth_jwt_algorithm ES512; - auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/ec_key_521-pub.pem"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/custom-header/hs256 { - auth_jwt_enabled on; - auth_jwt_redirect on; - auth_jwt_location HEADER=Auth-Token; - auth_jwt_algorithm HS256; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/request/sub { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_request_claims sub; - - add_header "Test" "sub=$http_jwt_sub"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/request/name-1 { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_request_claims firstName lastName; - - add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/request/name-2 { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_request_claims firstName; - auth_jwt_extract_request_claims lastName; - - add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/request/nested { - location /secure/extract-claim/request/nested { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_request_claims username; - - add_header "Test" "username=$http_jwt_username"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - } - - location /secure/extract-claim/response/sub { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_response_claims sub; - - add_header "Test" "sub=$sent_http_jwt_sub"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/response/name-1 { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_response_claims firstName lastName; - - add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/response/name-2 { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_response_claims firstName; - auth_jwt_extract_response_claims lastName; - - add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - - location /secure/extract-claim/response/nested { - location /secure/extract-claim/response/nested { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_response_claims username; - - add_header "Test" "username=$sent_http_jwt_username"; - - alias /usr/share/nginx/html/; - try_files index.html =404; - } - } - - location /secure/extract-claim/if/sub { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_var_claims sub; - - if ($jwt_claim_sub = 'some-long-uuid') { - return 200; - } - - return 401; - } - - location /secure/extract-claim/body/sub { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_extract_var_claims sub; - - return 200 "sub: $jwt_claim_sub"; - } - - location /secure/extract-claim/body/multiple { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_validate_sub on; - auth_jwt_extract_var_claims firstName middleName lastName; - - return 200 "you are: $jwt_claim_firstName $jwt_claim_middleName $jwt_claim_lastName"; - } - - location /profile { - auth_jwt_enabled on; - auth_jwt_redirect off; - auth_jwt_location HEADER=Authorization; - auth_jwt_validate_sub on; - - location /profile/me { - auth_jwt_extract_var_claims sub; - - return 301 /profile/$jwt_claim_sub; - } - } - - location /return-url { - auth_jwt_enabled on; - auth_jwt_redirect on; - } -} diff --git a/test/etc/nginx/ec_key_256-pub.pem b/test/etc/nginx/ec_key_256-pub.pem deleted file mode 100644 index 3306ea0..0000000 --- a/test/etc/nginx/ec_key_256-pub.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz -ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== ------END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_384-pub.pem b/test/etc/nginx/ec_key_384-pub.pem deleted file mode 100644 index e642ed1..0000000 --- a/test/etc/nginx/ec_key_384-pub.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 -aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 -LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu ------END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_521-pub.pem b/test/etc/nginx/ec_key_521-pub.pem deleted file mode 100644 index 0cb875c..0000000 --- a/test/etc/nginx/ec_key_521-pub.pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO -fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC -gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh -vXjq39xtcIBRTO1c2zs= ------END PUBLIC KEY----- diff --git a/test/etc/nginx/rsa_key_2048-pub.pem b/test/etc/nginx/rsa_key_2048-pub.pem deleted file mode 100755 index 01f59bf..0000000 --- a/test/etc/nginx/rsa_key_2048-pub.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh -uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ -iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM -ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g -6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe -K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t -BwIDAQAB ------END PUBLIC KEY----- diff --git a/test/etc/nginx/test.crt b/test/etc/nginx/test.crt deleted file mode 100644 index fb406ba..0000000 --- a/test/etc/nginx/test.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIUMG9M4Itu0cOyX0+La+7huiIoX6YwDQYJKoZIhvcNAQEL -BQAwcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQHDAxG -YWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4xFzAV -BgNVBAsMDk5HSU5YIEF1dGggSldUMB4XDTI0MDMxNTE4MTM1MloXDTM0MDMxMzE4 -MTM1MlowcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQH -DAxGYWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4x -FzAVBgNVBAsMDk5HSU5YIEF1dGggSldUMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAih41Ct5XgcSTz7ZVAjBb0t0z9Qae08aseoMEKJf7AmNqKtsvzeAw -/DJxOWJR5VPtUWhFAmXxPfG2B6aiSIVJVpG9yzcdQlCvyJG7Ub4QCm5GXwpU+zDC -qmD5ksz9QMdOzvRLypAU1ciZiCXjwpUnW+BZyZ9Tpmsxm6/gOzkd3rxoIbc9uXxp -5o4n6k02EPSzLzUhkZnhLQrOAGUB7+q11FAU5eNMlTWC9gQUsbNaTVtKmM2eV9BA -UHdX2GbkfFbN22l3Wey4oyNZWmye1ZFOPyBR+tyU3pofhb+R+hTFmeNBzrJq3i30 -Qi0B8AnulKdOjnTysPYjDTrN6xcVDWNmPQIDAQABo1MwUTAdBgNVHQ4EFgQUczdy -7s64NJHNGsQTf/zwFnQe6LMwHwYDVR0jBBgwFoAUczdy7s64NJHNGsQTf/zwFnQe -6LMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAfcxCiz6ShHof -lXiE2j+s556SM2n8oW/S1BSjFC2wF1uKVeMJA1gAaWObC3ElqffFlqTdCorhgRS/ -knWa+Sqe/jWBSgwLG/e5DvxXWjD7b7kZdAZNy9evs5nhVfcLT+GyvB/z5GdAFY7s -xYmLrC07ubhHIL9h7lhNKbRr++o+BcClQBZKRO4fxBwXxqx/rHudjH87Wr61Ov52 -90xNjwcqvevY0skmPao5+oyxkURdKZualNxiOGMPpywkpJkfl8Az5xKAJhUMAtFR -smhQduejEkcxfxtsiYgVoulI29GAsMr9zHps9zb5k0+SWIiSixjQ0CpRhLcNYu4F -QPgLQLGwUQ== ------END CERTIFICATE----- diff --git a/test/etc/nginx/test.key b/test/etc/nginx/test.key deleted file mode 100644 index 13ec754..0000000 --- a/test/etc/nginx/test.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCKHjUK3leBxJPP -tlUCMFvS3TP1Bp7Txqx6gwQol/sCY2oq2y/N4DD8MnE5YlHlU+1RaEUCZfE98bYH -pqJIhUlWkb3LNx1CUK/IkbtRvhAKbkZfClT7MMKqYPmSzP1Ax07O9EvKkBTVyJmI -JePClSdb4FnJn1OmazGbr+A7OR3evGghtz25fGnmjifqTTYQ9LMvNSGRmeEtCs4A -ZQHv6rXUUBTl40yVNYL2BBSxs1pNW0qYzZ5X0EBQd1fYZuR8Vs3baXdZ7LijI1la -bJ7VkU4/IFH63JTemh+Fv5H6FMWZ40HOsmreLfRCLQHwCe6Up06OdPKw9iMNOs3r -FxUNY2Y9AgMBAAECggEAAkwEggGp/xb67FCyDJ8rdimTZFPi9U7coUCN8HNI/qrf -lTnfvox0oOUUqMMmIIQeS/HJ4ANvZe8GO3QkE8R5Sg7F0yjZL2tyTCNPgOMCMK8E -mmHS58brHdrbm658C1ILnfmssjNmNueNbuW00Koa8imCsY2ZEW+L7vTKuMFqg6c+ -BDJxC4yoCPwSTVfcajjzI6FVfphE0pd8Ho/sE8vTqdmovh23+vgfNUq1L9Smvf7R -YLM+hS1ouRP2BI5AN0sm04Kxd8MKPzuwCxteoZ9Y9YHyr1JeWGTTL0T24+LwUee/ -24zXZFrzpTgmtDYeEuVWsF5bP/fMS4Fctda3pdJMsQKBgQDCANjGDwwfSCCev2kl -WdrFJywhn5hWLWFwlo/FwLOsFJtejaBwIDRQCMPZ74H+KMHwUnO3vTanKJWqDRP9 -CdMh94C1BqobRV6rN4HgA4Opxim1EyRWHV6ui41zokk2mJrwUzKkR8t9lt9EZKrk -ZPyKER9A4hBqBmYvaYxodN8U1QKBgQC2QXUQq9j7niT7t4xMi0e9vnPLs0z1yUK9 -0nzKwTHDPflk3o2sKvH7199qVkc15JQ9DQ7NuYD7ezLbE3DJuVzpNDAfNXmfWHmp -7ukdnxyn6ZCmzQY7/fTpJTEGKVQMVCgf2f5ANgxm5EmN0yWRMcEt1VXIwCisY56p -o6nwv/1fyQKBgQCJBnIVyjEEszwfBBEvCX0kvVtFUGUXkSv+isl3onkFNPTcXuoP -6B8q3FYAy1MkggMhTAthnqpIfLjhCCWzFspidl8Y/WEOq/uGsUjxQWowcr+onqGO -lWX3oKfDIb/WaQkeb5UYRYFr7jE6LGQrt0xL9HX/rOxtBqIMIN/EM7ARFQKBgDAJ -zMtaIFUh9+mJFafPRleS7X6RggV+yOKzqkTe6zjlCuk1Z+4rW6Df43lpyFdCKnh1 -CqPa805VyK/Jzf69pumo4c44EBiZ/2d1G2i9WZZAj+oHPE9vvq/9J5DSL98YB4Nt -uABAvsAYB/Mj5lEA5kQoaPYDADWABH/+LXrRf/1RAoGAUvxPvmpkGMC+KdmjLam7 -CPC3+y4MZOyZ11BhOxLhd1K2qcQd9K7tkjUhNxRn5GVzpzOKeFJFtiih2uN+PBNJ -oylPR03uk/7D52b1OYaJhs9bQkth//Qk935nyRM26C2vG4tQLfT/cFi5F53n0ZCQ -7e8O6+QY0lZnpvsfnt8YIsM= ------END PRIVATE KEY----- diff --git a/test/rsa_key_2048.pem b/test/rsa_key_2048.pem deleted file mode 100755 index 0f58120..0000000 --- a/test/rsa_key_2048.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDC2kwAziXUf33m -iqWp0yG6o259+nj7hpQLC4UT0Hmz0wmvreDJ/yNbSgOvsxvVdvzL2IaRZ+Gi5mo0 -lswWvL6IGz7PZO0kXTq9sdBnNqMOx27HddV9e/2/p0MgibJTbgywY2Sk23QYhJpq -Kq/nU0xlBfSaI5ddZ2RC9ZNkVeGawUKYksTruhAVJqviHN8BoK6VowP5vcxyyOWH -TK9KruDqzCIhqwRTeo0spokBkTN/LCuhVivcHAzUiJVtB4qAiTI9L/zkzhjpKz9P -45aLU54rj011gG8U/6E1USh5nMnPkr+d3oLfkhfS3Zs3kJVdyFQWZpQxiTaI92Fd -2wLvbS0HAgMBAAECggEAD8dTnkETSSjlzhRuI9loAtAXM3Zj86JLPLW7GgaoxEoT -n7lJ2bGicFMHB2ROnbOb9vnas82gtOtJsGaBslmoaCckp/C5T1eJWTEb+i+vdpPp -wZcmKZovyyRFSE4+NYlU17fEv6DRvuaGBpDcW7QgHJIl45F8QWEM+msee2KE+V4G -z/9vAQ+sOlvsb4mJP1tJIBx9Lb5loVREwCRy2Ha9tnWdDNar8EYkOn8si4snPT+E -3ZCy8mlcZyUkZeiS/HdtydxZfoiwrSRYamd1diQpPhWCeRteQ802a7ds0Y2YzgfF -UaYjNuRQm7zA//hwbXS7ELPyNMU15N00bajlG0tUOQKBgQDnLy01l20OneW6A2cI -DIDyYhy5O7uulsaEtJReUlcjEDMkin8b767q2VZHb//3ZH+ipnRYByUUyYUhdOs2 -DYRGGeAebnH8wpTT4FCYxUsIUpDfB7RwfdBONgaKewTJz/FPswy1Ye0b5H2c6vVi -m2FZ33HQcoZ3wvFFqyGVnMzpOwKBgQDXxL95yoxUGKa8vMzcE3Cn01szh0dFq0sq -cFpM+HWLVr84CItuG9H6L0KaStEEIOiJsxOVpcXfFFhsJvOGhMA4DQTwH4WuXmXp -1PoVMDlV65PYqvhzwL4+QhvZO2bsrEunITXOmU7CI6kilnAN3LuP4HbqZgoX9lqP -I31VYzLupQKBgGEYck9w0s/xxxtR9ILv5XRnepLdoJzaHHR991aKFKjYU/KD7JDK -INfoAhGs23+HCQhCCtkx3wQVA0Ii/erM0II0ueluD5fODX3TV2ZibnoHW2sgrEsW -vFcs36BnvIIaQMptc+f2QgSV+Z/fGsKYadG6Q+39O7au/HB7SHayzWkjAoGBAMgt -Fzslp9TpXd9iBWjzfCOnGUiP65Z+GWkQ/SXFqD+SRir0+m43zzGdoNvGJ23+Hd6K -TdQbDJ0uoe4MoQeepzoZEgi4JeykVUZ/uVfo+nh06yArVf8FxTm7WVzLGGzgV/uA -+wtl/cRtEyAsk1649yW/KHPEIP8kJdYAJeoO8xSlAoGAERMrkFR7KGYZG1eFNRdV -mJMq+Ibxyw8ks/CbiI+n3yUyk1U8962ol2Q0T4qjBmb26L5rrhNQhneM4e8mo9FX -LlQapYkPvkdrqW0Bp72A/UNAvcGTmN7z5OCJGMUutx2hmEAlrYmpLKS8pM/p9zpK -tEOtzsP5GMDYVlEp1jYSjzQ= ------END PRIVATE KEY----- diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile deleted file mode 100644 index 1065558..0000000 --- a/test/test-nginx.dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -ARG BASE_IMAGE - -FROM ${BASE_IMAGE:?required} AS NGINX -ARG PORT -ARG SSL_PORT - -COPY etc/ /etc/ - -COPY <<` /usr/share/nginx/html/index.html - - Test - -

NGINX Auth-JWT Module Test

- - -` - -RUN sed -i "s|%{PORT}|${PORT:?required}|" /etc/nginx/conf.d/test.conf -RUN sed -i "s|%{SSL_PORT}|${SSL_PORT:?required}|" /etc/nginx/conf.d/test.conf diff --git a/test/test-runner.dockerfile b/test/test-runner.dockerfile deleted file mode 100644 index 18fc3d3..0000000 --- a/test/test-runner.dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -ARG RUNNER_BASE_IMAGE - -FROM ${RUNNER_BASE_IMAGE:?required} -ARG PORT -ARG SSL_PORT - -ENV PORT=${PORT:?required} -ENV SSL_PORT=${SSL_PORT:?required} - -RUN <<` - set -e - apt-get update - apt-get install -y curl bash -` - -COPY test.sh . - -CMD ./test.sh diff --git a/test/test.sh b/test/test.sh deleted file mode 100755 index c726a75..0000000 --- a/test/test.sh +++ /dev/null @@ -1,387 +0,0 @@ -#!/bin/bash -eu - -# set a test # here to execute only that test and output additional info -DEBUG= - -RED='\e[31m' -GREEN='\e[32m' -GRAY='\e[90m' -NC='\e[00m' - -NUM_TESTS=0; -NUM_SKIPPED=0; -NUM_FAILED=0; - -run_test () { - NUM_TESTS=$((${NUM_TESTS} + 1)); - - if [ "${DEBUG}" == '' ] || [ ${DEBUG} == ${NUM_TESTS} ]; then - local OPTIND; - local name= - local path= - local expectedCode= - local expectedResponseRegex= - local extraCurlOpts= - local scheme='http' - local port=${PORT} - local curlCommand= - local exitCode= - local response= - local testNum="${GRAY}${NUM_TESTS}${NC}\t" - - while getopts "n:asp:r:c:x:" option; do - case $option in - n) - name=$OPTARG;; - s) - scheme='https' - port=${SSL_PORT};; - p) - path=$OPTARG;; - c) - expectedCode=$OPTARG;; - r) - expectedResponseRegex=$OPTARG;; - x) - extraCurlOpts=$OPTARG;; - \?) # Invalid option - printf "Error: Invalid option\n"; - exit;; - esac - done - - curlCommand="curl -skv ${scheme}://nginx:${port}${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" - response=$(eval "${curlCommand}") - exitCode=$? - - printf "\n${testNum}" - - if [ "${exitCode}" -ne "0" ]; then - printf "${RED}${name} -- unexpected exit code from cURL\n\tcURL Exit Code: ${exitCode}"; - NUM_FAILED=$((${NUM_FAILED} + 1)); - else - local okay=1 - - if [ "${expectedCode}" != "" ]; then - local responseCode=$(echo "${response}" | grep -Eo 'HTTP/1.1 ([0-9]{3})' | awk '{print $2}') - - if [ "${expectedCode}" != "${responseCode}" ]; then - printf "${RED}${name} -- unexpected status code\n\tExpected: ${expectedCode}\n\tActual: ${responseCode}\n\tPath: ${path}" - NUM_FAILED=$((${NUM_FAILED} + 1)) - okay=0 - fi - fi - - if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ ${expectedResponseRegex} ]]; then - printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex//%/%%}" - NUM_FAILED=$((${NUM_FAILED} + 1)) - okay=0 - fi - - if [ "${okay}" == '1' ]; then - printf "${GREEN}${name}"; - fi - fi - - if [ "${DEBUG}" == "${NUM_TESTS}" ]; then - printf '\n\tcURL Command: %s' "${curlCommand:---}" - printf '\n\tResponse: %s' "${response:---}" - fi - - printf "${NC}\n" - else - NUM_SKIPPED=$((${NUM_SKIPPED} + 1)) - fi -} - -main() { - local JWT_HS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.r8tG8IZheiQ-i6HqUYyJj9V6dipgcQ4ZIdxau6QCZDo - local JWT_HS256_MISSING_SUB=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q - local JWT_HS256_MISSING_EMAIL=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM - local JWT_HS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.SS57j7PEybjbsp3g5W-IhhJHBmG5K-97qvgBKL16xj9ey-uMeEenWjGbB2vVp0kq - local JWT_HS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.xtSU6EWN2LILVsYzJFJpKnRkqjn_3qjz-J2ttNKnhZ60_5YjFeC8io4k8k1u77zlohSWvWMdugD9ZaB3vjJo-w - local JWT_RS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ - local JWT_RS256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 - local JWT_RS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.H35bTcZRhepWIoa8pKCbUMRuAOkVX9K5hJjc6tPmQwWmTw8lrktsvmMzJg_rgqnJLnAkciSIQw5EDj7fngS5zX2ThyRxrkPuE2Uiyw2Ect-mo9Kg1lrWgnyZCuCgq-Up9HQRAv0160mePlm8Gs4TOY6CPr38zwTcDZsy_Keq93igDQV8WuuWAGICaGd5ZyUOPjjzGShRjTU8Szz7fnpZpTtYRCYVo0pc5yfRWYm0fdn-4AseyGvd8JJ2xfnAEe4kZOkz7X1MLKtL0slKg3m2PH1lD7HwxIawXRTPWxArhJ9dcTNiDUrqtde2juGwOuMD_zTsb2Jj0_rmRb0Q6aljNw - local JWT_RS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.iUupyKypfXJ5aZWfItSW-mOmx9a4C4X7Yr5p5Fk8W75ZhkOq0EeNfstTxx870brhkdPovBhO2LYI44_HoH9XicQNL6JnFprE0r61eJFngbuzlhRQiWpq0xYrazJWc9zB7_GgL2ZCwtw-Ts3G23Q0632wVm6-d7MKvG7RS8aEjN-MuVGdtLglH3forpItmFxw-if40EQsBL7hncN_XNcQTO4KPHkqmlpac_oKXRrLFDIIt2tB6OOpvY4QcpERoxexp4pi2f-JoINnWX_dU5JnIs3ypVJLQPfoJvxg8fsg3zYrOvMYnfsqOCYoHtZGK0O7jyfFmcGo5v2hLT-CpoF3Zw - local JWT_ES256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.WFfJXGr5whKHB7arjsTXPTJ6TAsS1LoRxu7Vj2_HrLaIQphWJM6BICf-M3cv52tFzt-XTZb6GxlDgAbHo8z9Zg - local JWT_ES256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 - local JWT_ES384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._EFxXYOTAfT3gB3xUfgGR2UyXHeRTlDWqA94oZbB0DDa7YPZTEX9T4C_0ylnOFKZ6irGHZA8vxjgXDH3DZKWwBWcZ-XaQ_Q4Ws2J-AEeLqcl7_CS6q9mFo0Y7vUNEn-W - local JWT_ES512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.AFY4gNCtZNYkrTiijDkV4eKIt2UPMIuJBfZIk69jgI8FSGCQyUIMmIVg0fTvbaSiaryXzcjbG5TCm8a9Vu3KFJutAHGrgvZqcdklxx6Fbk3an3r_CH68n_ncwS3SUV58mDjf0OX8jRuNdudU1L5xYNQdodo-fxPIb1oHXfMJ0CmULDR9 - - run_test -n 'when auth disabled, should return 200' \ - -p '/' \ - -c '200' - - run_test -n '[SSL] when auth disabled, should return 200' \ - -s \ - -p '/' \ - -c '200' - - run_test -n 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ - -p '/secure/auth-header/default' \ - -c '302' - - run_test -n '[SSL] when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ - -s \ - -p '/secure/auth-header/default' \ - -c '302' - - run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 200' \ - -p '/secure/auth-header/default/no-redirect' \ - -c '200' \ - -x "--header \"Authorization: ${JWT_HS256_VALID}\"" - - run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header with Bearer, should return 200' \ - -p '/secure/auth-header/default/no-redirect' \ - -c '200' \ - -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" - - run_test -n 'when auth enabled with Authorization header with Bearer, should keep header intact' \ - -p '/secure/auth-header/default/proxy-header' \ - -c '200' \ - -r "< Test-Authorization: Bearer ${JWT_HS256_VALID}" \ - -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" - - run_test -n 'when auth enabled with Authorization header with Bearer, lower-case "bearer" should be accepted' \ - -p '/secure/auth-header/default/proxy-header' \ - -c '200' \ - -r "< Test-Authorization: bearer ${JWT_HS256_VALID}" \ - -x "--header \"Authorization: bearer ${JWT_HS256_VALID}\"" - - run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ - -p '/secure/cookie/default' \ - -c '302' - - run_test -n 'when auth enabled with default algorithm with no redirect and no JWT cookie, should return 401' \ - -p '/secure/cookie/default/no-redirect' \ - -c '401' - - run_test -n 'when auth enabled with default algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/default' \ - -c '200' \ - -x "--cookie jwt=${JWT_HS256_VALID}" - - run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub, returns 200' \ - -p '/secure/cookie/default' \ - -c '200' \ - -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' - - run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub when sub validated, returns 302' \ - -p '/secure/cookie/default/validate-sub' \ - -c '302' \ - -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' - - run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no email, returns 200' \ - -p '/secure/cookie/default' \ - -c '200' \ - -x ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' - - run_test -n 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/hs256' \ - -c '200' \ - -x '--cookie "jwt=${JWT_HS256_VALID}"' - - run_test -n 'when auth enabled with HS384 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/hs384' \ - -c '200' \ - -x '--cookie "jwt=${JWT_HS384_VALID}"' - - run_test -n 'when auth enabled with HS512 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/hs512' \ - -c '200' \ - -x '--cookie "jwt=${JWT_HS512_VALID}"' - - run_test -n 'when auth enabled with RS256 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/rs256' \ - -c '200' \ - -x ' --cookie "jwt=${JWT_RS256_VALID}"' - - run_test -n 'when auth enabled with ES256 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/es256' \ - -c '200' \ - -x ' --cookie "jwt=${JWT_ES256_VALID}"' - - run_test -n 'when auth enabled with ES384 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/es384' \ - -c '200' \ - -x ' --cookie "jwt=${JWT_ES384_VALID}"' - - run_test -n 'when auth enabled with ES512 algorithm and valid JWT cookie, returns 200' \ - -p '/secure/cookie/es512' \ - -c '200' \ - -x ' --cookie "jwt=${JWT_ES512_VALID}"' - - run_test -n 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ - -p '/secure/auth-header/rs256/file' \ - -c '200' \ - -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' - - run_test -n 'when auth enabled with RS256 algorithm via file and invalid JWT in Authorization header, returns 401' \ - -p '/secure/auth-header/rs256/file' \ - -c '302' \ - -x '--header "Authorization: Bearer ${JWT_RS256_INVALID}"' - - run_test -n 'when auth enabled with RS384 algorithm via file and valid JWT in Authorization header, returns 200' \ - -p '/secure/auth-header/rs384/file' \ - -c '200' \ - -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' - - run_test -n 'when auth enabled with RS512 algorithm via file and valid JWT in Authorization header, returns 200' \ - -p '/secure/auth-header/rs512/file' \ - -c '200' \ - -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' - - run_test -n 'when auth enabled with ES256 algorithm via file and valid JWT in Authorization header, returns 200' \ - -p '/secure/auth-header/es256/file' \ - -c '200' \ - -x '--header "Authorization: Bearer ${JWT_ES256_VALID}"' - - run_test -n 'when auth enabled with ES256 algorithm via file and invalid JWT in Authorization header, returns 401' \ - -p '/secure/auth-header/es256/file' \ - -c '302' \ - -x '--header "Authorization: Bearer ${JWT_ES256_INVALID}"' - - run_test -n 'when auth enabled with ES384 algorithm via file and valid JWT in Authorization header, returns 200' \ - -p '/secure/auth-header/es384/file' \ - -c '200' \ - -x '--header "Authorization: Bearer ${JWT_ES384_VALID}"' - - run_test -n 'when auth enabled with ES512 algorithm via file and valid JWT in Authorization header, returns 200' \ - -p '/secure/auth-header/es512/file' \ - -c '200' \ - -x '--header "Authorization: Bearer ${JWT_ES512_VALID}"' - - run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header without bearer, returns 200' \ - -p '/secure/custom-header/hs256/' \ - -c '200' \ - -x '--header "Auth-Token: ${JWT_HS256_VALID}"' - - run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header with bearer, returns 200' \ - -p '/secure/custom-header/hs256/' \ - -c '200' \ - -x '--header "Auth-Token: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts single claim to request variable' \ - -p '/secure/extract-claim/request/sub' \ - -r '< Test: sub=some-long-uuid' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims (single directive) to request variable' \ - -p '/secure/extract-claim/request/name-1' \ - -r '< Test: firstName=hello; lastName=world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims (multiple directives) to request variable' \ - -p '/secure/extract-claim/request/name-2' \ - -r '< Test: firstName=hello; lastName=world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts nested claim to request variable' \ - -p '/secure/extract-claim/request/nested' \ - -r '< Test: username=hello\.world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts single claim to response variable' \ - -p '/secure/extract-claim/response/sub' \ - -r '< Test: sub=some-long-uuid' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims (single directive) to response variable' \ - -p '/secure/extract-claim/response/name-1' \ - -r '< Test: firstName=hello; lastName=world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims (multiple directives) to response variable' \ - -p '/secure/extract-claim/response/name-2' \ - -r '< Test: firstName=hello; lastName=world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts nested claim to response variable' \ - -p '/secure/extract-claim/response/nested' \ - -r '< Test: username=hello.world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts single claim to response header' \ - -p '/secure/extract-claim/response/sub' \ - -r '< JWT-sub: some-long-uuid' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims (single directive) to response header' \ - -p '/secure/extract-claim/response/name-1' \ - -r '< JWT-firstName: hello' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims (multiple directives) to response header' \ - -p '/secure/extract-claim/response/name-2' \ - -r '< JWT-firstName: hello' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts nested claim to response header' \ - -p '/secure/extract-claim/response/nested' \ - -r '< JWT-username: hello\.world' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'tests single claim with if statement' \ - -p '/secure/extract-claim/if/sub' \ - -c 200 \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'tests absence of single claim with if statement' \ - -p '/secure/extract-claim/if/sub' \ - -c 401 \ - -x '--header "Authorization: Bearer ${JWT_HS256_MISSING_SUB}"' - - run_test -n 'extracts single claim to response body' \ - -p '/secure/extract-claim/body/sub' \ - -c 200 \ - -r 'sub: some-long-uuid$' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'extracts multiple claims to response body' \ - -p '/secure/extract-claim/body/multiple' \ - -c 200 \ - -r 'you are: hello world$' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'redirect based on claim' \ - -p '/profile/me' \ - -c 301 \ - -r '< Location: http://nginx:8000/profile/some-long-uuid' \ - -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' - - run_test -n 'returns 302 if auth enabled and no JWT provided' \ - -p '/return-url' \ - -c '302' - - run_test -n 'redirects to login if auth enabled and no JWT provided' \ - -p '/return-url' \ - -r '< Location: https://example\.com/login.*' - - run_test -n 'adds return_url to login URL when redirected to login' \ - -p '/return-url' \ - -r '< Location: https://example\.com/login\?return_url=http://nginx.*' - - run_test -n 'return_url includes port when redirected to login' \ - -p '/return-url' \ - -r "< Location: https://example\.com/login\?return_url=http://nginx:${PORT}/return-url" - - run_test -n 'return_url includes query when redirected to login' \ - -p '/return-url?test=123' \ - -r '< Location: https://example\.com/login\?return_url=http://nginx.*/return-url%3Ftest=123' - - if [[ "${NUM_FAILED}" = '0' ]]; then - printf "\nRan ${NUM_TESTS} tests successfully (skipped ${NUM_SKIPPED}).\n" - return 0 - else - printf "\nRan ${NUM_TESTS} tests: ${GREEN}$((${NUM_TESTS} - ${NUM_FAILED})) passed${NC}; ${RED}${NUM_FAILED} failed${NC}; ${NUM_SKIPPED} skipped\n" - return 1 - fi -} - -if [ "${DEBUG}" != '' ]; then - printf "\n${RED}Some tests will be skipped since DEBUG is set.${NC}\n" -fi - -printf "\n${GRAY}Starting tests using port ${PORT}...${NC}\n" -main From c415b81e28b5fdacddc5716fb7e21728a4c40096 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Wed, 5 Feb 2025 16:07:30 -0500 Subject: [PATCH 123/130] fix incorrect function name --- .bin/git/hooks/pre-push-build-and-test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bin/git/hooks/pre-push-build-and-test b/.bin/git/hooks/pre-push-build-and-test index 9cf4682..876ca32 100755 --- a/.bin/git/hooks/pre-push-build-and-test +++ b/.bin/git/hooks/pre-push-build-and-test @@ -4,7 +4,7 @@ REPO_ROOT_DIR=$(git rev-parse --show-toplevel) CHANGE_COUNT=$(cd ${REPO_ROOT_DIR}; git diff --name-only origin/HEAD..HEAD -- resources/ src/ test/ Dockerfile scripts.sh |wc -l) if [[ "0" -ne "${CHANGE_COUNT}" ]]; then - (cd ${REPO_ROOT_DIR}; ./scripts.sh rebuild_nginx rebuild_test_runner test) + (cd ${REPO_ROOT_DIR}; ./scripts.sh rebuild_nginx rebuild_test test) else HOOK_NAME=$(basename $0) From a1662419010034a8230892489b083365bed4232c Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 18 Feb 2025 11:15:25 -0500 Subject: [PATCH 124/130] restore accidentally-deleted test dir :-| --- test/docker-compose-test.yml | 19 ++ test/ec_key_256.pem | 5 + test/ec_key_384.pem | 6 + test/ec_key_521.pem | 8 + test/etc/nginx/conf.d/test.conf | 448 ++++++++++++++++++++++++++++ test/etc/nginx/ec_key_256-pub.pem | 4 + test/etc/nginx/ec_key_384-pub.pem | 5 + test/etc/nginx/ec_key_521-pub.pem | 6 + test/etc/nginx/rsa_key_2048-pub.pem | 9 + test/etc/nginx/test.crt | 23 ++ test/etc/nginx/test.key | 28 ++ test/rsa_key_2048.pem | 28 ++ test/test-nginx.dockerfile | 19 ++ test/test-runner.dockerfile | 18 ++ test/test.sh | 387 ++++++++++++++++++++++++ 15 files changed, 1013 insertions(+) create mode 100644 test/docker-compose-test.yml create mode 100644 test/ec_key_256.pem create mode 100644 test/ec_key_384.pem create mode 100644 test/ec_key_521.pem create mode 100644 test/etc/nginx/conf.d/test.conf create mode 100644 test/etc/nginx/ec_key_256-pub.pem create mode 100644 test/etc/nginx/ec_key_384-pub.pem create mode 100644 test/etc/nginx/ec_key_521-pub.pem create mode 100755 test/etc/nginx/rsa_key_2048-pub.pem create mode 100644 test/etc/nginx/test.crt create mode 100644 test/etc/nginx/test.key create mode 100755 test/rsa_key_2048.pem create mode 100644 test/test-nginx.dockerfile create mode 100644 test/test-runner.dockerfile create mode 100755 test/test.sh diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml new file mode 100644 index 0000000..72ff710 --- /dev/null +++ b/test/docker-compose-test.yml @@ -0,0 +1,19 @@ +services: + + nginx: + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-nginx + build: + context: . + dockerfile: test-nginx.dockerfile + args: + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} + logging: + driver: ${LOG_DRIVER:-journald} + + runner: + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-runner + build: + context: . + dockerfile: test-runner.dockerfile + depends_on: + - nginx \ No newline at end of file diff --git a/test/ec_key_256.pem b/test/ec_key_256.pem new file mode 100644 index 0000000..4206969 --- /dev/null +++ b/test/ec_key_256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOlEBGcZxxhv8FkN0 +YIvax6fnhJbMeotzIEBxIglkNu6hRANCAATP1NpDzvZmKd2Mw6hIrv4nzUfNu7OK +mT5VuL5LhvUgzTqVGuxwevA7DlFsNVSfCljIBG3geio3fcd4k0Z9SygL +-----END PRIVATE KEY----- diff --git a/test/ec_key_384.pem b/test/ec_key_384.pem new file mode 100644 index 0000000..2aa5780 --- /dev/null +++ b/test/ec_key_384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDADyrL6llSQoQOZ/PF/ +l761kAbrTwn4vu30Kr34ScW6bRKVXLq3cT3QssJ1nF9B63qhZANiAAQ48dOfIEd3 +0TCVE0JT4ZU0Db7Ftz+ex7lojP7uqTY9OI59yoMB01zUN4JK30BRXS9Yv0A9Bu1z +fgLu93FSn0kd0zIPMvuu5LUt60M/miSt2lA0OrqFhKjx6FFdN/lNh64= +-----END PRIVATE KEY----- diff --git a/test/ec_key_521.pem b/test/ec_key_521.pem new file mode 100644 index 0000000..10471dc --- /dev/null +++ b/test/ec_key_521.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAKkag6aVn4XAbaALo +0b3pypdP5RBX7uKxHmKlkNCcpA0oVTdgjnM5NpJP8ZOM6NjVhEzsn6c/Tdn8hL8w +SI55hFWhgYkDgYYABABpTipSvbs8fq44u4fA+v7DTNYViA58sqbrxjxdzwWZ8eEj +CXsH7yzSGx3Y19NSyrX8HbjWmrj5uxiKeFCB8mGzTwDcFIKCMeMkHjZs/fmVOumR +a2XSpj7BP6wqcN6Pf+UqECivGAZGRHoabo/dm5zF9M3gO+G9eOrf3G1wgFFM7Vzb +Ow== +-----END PRIVATE KEY----- diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf new file mode 100644 index 0000000..4e5d764 --- /dev/null +++ b/test/etc/nginx/conf.d/test.conf @@ -0,0 +1,448 @@ +error_log /var/log/nginx/debug.log debug; +access_log /var/log/nginx/access.log; + +server { + listen %{PORT}; + listen %{SSL_PORT} ssl; + server_name localhost; + + ssl_certificate /etc/nginx/test.crt; + ssl_certificate_key /etc/nginx/test.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; + auth_jwt_loginurl "https://example.com/login"; + auth_jwt_enabled off; + + location / { + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default/validate-sub { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validate_sub on; + auth_jwt_location COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default/no-redirect { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm HS256; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm HS384; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm HS512; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES256; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES384; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES512; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default/no-redirect { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default/proxy-header { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + + add_header "Test-Authorization" "$http_authorization"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh +uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ +iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM +ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g +6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe +K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t +BwIDAQAB +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs256/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm RS256; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs384/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm RS384; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs512/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm RS512; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es256/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES256; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec_key_256-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es384/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES384; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec_key_384-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es512/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES512; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec_key_521-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/custom-header/hs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Auth-Token; + auth_jwt_algorithm HS256; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims sub; + + add_header "Test" "sub=$http_jwt_sub"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/name-1 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims firstName lastName; + + add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/name-2 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims firstName; + auth_jwt_extract_request_claims lastName; + + add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/nested { + location /secure/extract-claim/request/nested { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims username; + + add_header "Test" "username=$http_jwt_username"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + } + + location /secure/extract-claim/response/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims sub; + + add_header "Test" "sub=$sent_http_jwt_sub"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/name-1 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims firstName lastName; + + add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/name-2 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims firstName; + auth_jwt_extract_response_claims lastName; + + add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/nested { + location /secure/extract-claim/response/nested { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims username; + + add_header "Test" "username=$sent_http_jwt_username"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + } + + location /secure/extract-claim/if/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_var_claims sub; + + if ($jwt_claim_sub = 'some-long-uuid') { + return 200; + } + + return 401; + } + + location /secure/extract-claim/body/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_var_claims sub; + + return 200 "sub: $jwt_claim_sub"; + } + + location /secure/extract-claim/body/multiple { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_validate_sub on; + auth_jwt_extract_var_claims firstName middleName lastName; + + return 200 "you are: $jwt_claim_firstName $jwt_claim_middleName $jwt_claim_lastName"; + } + + location /profile { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_validate_sub on; + + location /profile/me { + auth_jwt_extract_var_claims sub; + + return 301 /profile/$jwt_claim_sub; + } + } + + location /return-url { + auth_jwt_enabled on; + auth_jwt_redirect on; + } +} diff --git a/test/etc/nginx/ec_key_256-pub.pem b/test/etc/nginx/ec_key_256-pub.pem new file mode 100644 index 0000000..3306ea0 --- /dev/null +++ b/test/etc/nginx/ec_key_256-pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_384-pub.pem b/test/etc/nginx/ec_key_384-pub.pem new file mode 100644 index 0000000..e642ed1 --- /dev/null +++ b/test/etc/nginx/ec_key_384-pub.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_521-pub.pem b/test/etc/nginx/ec_key_521-pub.pem new file mode 100644 index 0000000..0cb875c --- /dev/null +++ b/test/etc/nginx/ec_key_521-pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/rsa_key_2048-pub.pem b/test/etc/nginx/rsa_key_2048-pub.pem new file mode 100755 index 0000000..01f59bf --- /dev/null +++ b/test/etc/nginx/rsa_key_2048-pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh +uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ +iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM +ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g +6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe +K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t +BwIDAQAB +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/test.crt b/test/etc/nginx/test.crt new file mode 100644 index 0000000..fb406ba --- /dev/null +++ b/test/etc/nginx/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIUMG9M4Itu0cOyX0+La+7huiIoX6YwDQYJKoZIhvcNAQEL +BQAwcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQHDAxG +YWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4xFzAV +BgNVBAsMDk5HSU5YIEF1dGggSldUMB4XDTI0MDMxNTE4MTM1MloXDTM0MDMxMzE4 +MTM1MlowcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQH +DAxGYWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4x +FzAVBgNVBAsMDk5HSU5YIEF1dGggSldUMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAih41Ct5XgcSTz7ZVAjBb0t0z9Qae08aseoMEKJf7AmNqKtsvzeAw +/DJxOWJR5VPtUWhFAmXxPfG2B6aiSIVJVpG9yzcdQlCvyJG7Ub4QCm5GXwpU+zDC +qmD5ksz9QMdOzvRLypAU1ciZiCXjwpUnW+BZyZ9Tpmsxm6/gOzkd3rxoIbc9uXxp +5o4n6k02EPSzLzUhkZnhLQrOAGUB7+q11FAU5eNMlTWC9gQUsbNaTVtKmM2eV9BA +UHdX2GbkfFbN22l3Wey4oyNZWmye1ZFOPyBR+tyU3pofhb+R+hTFmeNBzrJq3i30 +Qi0B8AnulKdOjnTysPYjDTrN6xcVDWNmPQIDAQABo1MwUTAdBgNVHQ4EFgQUczdy +7s64NJHNGsQTf/zwFnQe6LMwHwYDVR0jBBgwFoAUczdy7s64NJHNGsQTf/zwFnQe +6LMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAfcxCiz6ShHof +lXiE2j+s556SM2n8oW/S1BSjFC2wF1uKVeMJA1gAaWObC3ElqffFlqTdCorhgRS/ +knWa+Sqe/jWBSgwLG/e5DvxXWjD7b7kZdAZNy9evs5nhVfcLT+GyvB/z5GdAFY7s +xYmLrC07ubhHIL9h7lhNKbRr++o+BcClQBZKRO4fxBwXxqx/rHudjH87Wr61Ov52 +90xNjwcqvevY0skmPao5+oyxkURdKZualNxiOGMPpywkpJkfl8Az5xKAJhUMAtFR +smhQduejEkcxfxtsiYgVoulI29GAsMr9zHps9zb5k0+SWIiSixjQ0CpRhLcNYu4F +QPgLQLGwUQ== +-----END CERTIFICATE----- diff --git a/test/etc/nginx/test.key b/test/etc/nginx/test.key new file mode 100644 index 0000000..13ec754 --- /dev/null +++ b/test/etc/nginx/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCKHjUK3leBxJPP +tlUCMFvS3TP1Bp7Txqx6gwQol/sCY2oq2y/N4DD8MnE5YlHlU+1RaEUCZfE98bYH +pqJIhUlWkb3LNx1CUK/IkbtRvhAKbkZfClT7MMKqYPmSzP1Ax07O9EvKkBTVyJmI +JePClSdb4FnJn1OmazGbr+A7OR3evGghtz25fGnmjifqTTYQ9LMvNSGRmeEtCs4A +ZQHv6rXUUBTl40yVNYL2BBSxs1pNW0qYzZ5X0EBQd1fYZuR8Vs3baXdZ7LijI1la +bJ7VkU4/IFH63JTemh+Fv5H6FMWZ40HOsmreLfRCLQHwCe6Up06OdPKw9iMNOs3r +FxUNY2Y9AgMBAAECggEAAkwEggGp/xb67FCyDJ8rdimTZFPi9U7coUCN8HNI/qrf +lTnfvox0oOUUqMMmIIQeS/HJ4ANvZe8GO3QkE8R5Sg7F0yjZL2tyTCNPgOMCMK8E +mmHS58brHdrbm658C1ILnfmssjNmNueNbuW00Koa8imCsY2ZEW+L7vTKuMFqg6c+ +BDJxC4yoCPwSTVfcajjzI6FVfphE0pd8Ho/sE8vTqdmovh23+vgfNUq1L9Smvf7R +YLM+hS1ouRP2BI5AN0sm04Kxd8MKPzuwCxteoZ9Y9YHyr1JeWGTTL0T24+LwUee/ +24zXZFrzpTgmtDYeEuVWsF5bP/fMS4Fctda3pdJMsQKBgQDCANjGDwwfSCCev2kl +WdrFJywhn5hWLWFwlo/FwLOsFJtejaBwIDRQCMPZ74H+KMHwUnO3vTanKJWqDRP9 +CdMh94C1BqobRV6rN4HgA4Opxim1EyRWHV6ui41zokk2mJrwUzKkR8t9lt9EZKrk +ZPyKER9A4hBqBmYvaYxodN8U1QKBgQC2QXUQq9j7niT7t4xMi0e9vnPLs0z1yUK9 +0nzKwTHDPflk3o2sKvH7199qVkc15JQ9DQ7NuYD7ezLbE3DJuVzpNDAfNXmfWHmp +7ukdnxyn6ZCmzQY7/fTpJTEGKVQMVCgf2f5ANgxm5EmN0yWRMcEt1VXIwCisY56p +o6nwv/1fyQKBgQCJBnIVyjEEszwfBBEvCX0kvVtFUGUXkSv+isl3onkFNPTcXuoP +6B8q3FYAy1MkggMhTAthnqpIfLjhCCWzFspidl8Y/WEOq/uGsUjxQWowcr+onqGO +lWX3oKfDIb/WaQkeb5UYRYFr7jE6LGQrt0xL9HX/rOxtBqIMIN/EM7ARFQKBgDAJ +zMtaIFUh9+mJFafPRleS7X6RggV+yOKzqkTe6zjlCuk1Z+4rW6Df43lpyFdCKnh1 +CqPa805VyK/Jzf69pumo4c44EBiZ/2d1G2i9WZZAj+oHPE9vvq/9J5DSL98YB4Nt +uABAvsAYB/Mj5lEA5kQoaPYDADWABH/+LXrRf/1RAoGAUvxPvmpkGMC+KdmjLam7 +CPC3+y4MZOyZ11BhOxLhd1K2qcQd9K7tkjUhNxRn5GVzpzOKeFJFtiih2uN+PBNJ +oylPR03uk/7D52b1OYaJhs9bQkth//Qk935nyRM26C2vG4tQLfT/cFi5F53n0ZCQ +7e8O6+QY0lZnpvsfnt8YIsM= +-----END PRIVATE KEY----- diff --git a/test/rsa_key_2048.pem b/test/rsa_key_2048.pem new file mode 100755 index 0000000..0f58120 --- /dev/null +++ b/test/rsa_key_2048.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDC2kwAziXUf33m +iqWp0yG6o259+nj7hpQLC4UT0Hmz0wmvreDJ/yNbSgOvsxvVdvzL2IaRZ+Gi5mo0 +lswWvL6IGz7PZO0kXTq9sdBnNqMOx27HddV9e/2/p0MgibJTbgywY2Sk23QYhJpq +Kq/nU0xlBfSaI5ddZ2RC9ZNkVeGawUKYksTruhAVJqviHN8BoK6VowP5vcxyyOWH +TK9KruDqzCIhqwRTeo0spokBkTN/LCuhVivcHAzUiJVtB4qAiTI9L/zkzhjpKz9P +45aLU54rj011gG8U/6E1USh5nMnPkr+d3oLfkhfS3Zs3kJVdyFQWZpQxiTaI92Fd +2wLvbS0HAgMBAAECggEAD8dTnkETSSjlzhRuI9loAtAXM3Zj86JLPLW7GgaoxEoT +n7lJ2bGicFMHB2ROnbOb9vnas82gtOtJsGaBslmoaCckp/C5T1eJWTEb+i+vdpPp +wZcmKZovyyRFSE4+NYlU17fEv6DRvuaGBpDcW7QgHJIl45F8QWEM+msee2KE+V4G +z/9vAQ+sOlvsb4mJP1tJIBx9Lb5loVREwCRy2Ha9tnWdDNar8EYkOn8si4snPT+E +3ZCy8mlcZyUkZeiS/HdtydxZfoiwrSRYamd1diQpPhWCeRteQ802a7ds0Y2YzgfF +UaYjNuRQm7zA//hwbXS7ELPyNMU15N00bajlG0tUOQKBgQDnLy01l20OneW6A2cI +DIDyYhy5O7uulsaEtJReUlcjEDMkin8b767q2VZHb//3ZH+ipnRYByUUyYUhdOs2 +DYRGGeAebnH8wpTT4FCYxUsIUpDfB7RwfdBONgaKewTJz/FPswy1Ye0b5H2c6vVi +m2FZ33HQcoZ3wvFFqyGVnMzpOwKBgQDXxL95yoxUGKa8vMzcE3Cn01szh0dFq0sq +cFpM+HWLVr84CItuG9H6L0KaStEEIOiJsxOVpcXfFFhsJvOGhMA4DQTwH4WuXmXp +1PoVMDlV65PYqvhzwL4+QhvZO2bsrEunITXOmU7CI6kilnAN3LuP4HbqZgoX9lqP +I31VYzLupQKBgGEYck9w0s/xxxtR9ILv5XRnepLdoJzaHHR991aKFKjYU/KD7JDK +INfoAhGs23+HCQhCCtkx3wQVA0Ii/erM0II0ueluD5fODX3TV2ZibnoHW2sgrEsW +vFcs36BnvIIaQMptc+f2QgSV+Z/fGsKYadG6Q+39O7au/HB7SHayzWkjAoGBAMgt +Fzslp9TpXd9iBWjzfCOnGUiP65Z+GWkQ/SXFqD+SRir0+m43zzGdoNvGJ23+Hd6K +TdQbDJ0uoe4MoQeepzoZEgi4JeykVUZ/uVfo+nh06yArVf8FxTm7WVzLGGzgV/uA ++wtl/cRtEyAsk1649yW/KHPEIP8kJdYAJeoO8xSlAoGAERMrkFR7KGYZG1eFNRdV +mJMq+Ibxyw8ks/CbiI+n3yUyk1U8962ol2Q0T4qjBmb26L5rrhNQhneM4e8mo9FX +LlQapYkPvkdrqW0Bp72A/UNAvcGTmN7z5OCJGMUutx2hmEAlrYmpLKS8pM/p9zpK +tEOtzsP5GMDYVlEp1jYSjzQ= +-----END PRIVATE KEY----- diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile new file mode 100644 index 0000000..1065558 --- /dev/null +++ b/test/test-nginx.dockerfile @@ -0,0 +1,19 @@ +ARG BASE_IMAGE + +FROM ${BASE_IMAGE:?required} AS NGINX +ARG PORT +ARG SSL_PORT + +COPY etc/ /etc/ + +COPY <<` /usr/share/nginx/html/index.html + + Test + +

NGINX Auth-JWT Module Test

+ + +` + +RUN sed -i "s|%{PORT}|${PORT:?required}|" /etc/nginx/conf.d/test.conf +RUN sed -i "s|%{SSL_PORT}|${SSL_PORT:?required}|" /etc/nginx/conf.d/test.conf diff --git a/test/test-runner.dockerfile b/test/test-runner.dockerfile new file mode 100644 index 0000000..18fc3d3 --- /dev/null +++ b/test/test-runner.dockerfile @@ -0,0 +1,18 @@ +ARG RUNNER_BASE_IMAGE + +FROM ${RUNNER_BASE_IMAGE:?required} +ARG PORT +ARG SSL_PORT + +ENV PORT=${PORT:?required} +ENV SSL_PORT=${SSL_PORT:?required} + +RUN <<` + set -e + apt-get update + apt-get install -y curl bash +` + +COPY test.sh . + +CMD ./test.sh diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..c726a75 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,387 @@ +#!/bin/bash -eu + +# set a test # here to execute only that test and output additional info +DEBUG= + +RED='\e[31m' +GREEN='\e[32m' +GRAY='\e[90m' +NC='\e[00m' + +NUM_TESTS=0; +NUM_SKIPPED=0; +NUM_FAILED=0; + +run_test () { + NUM_TESTS=$((${NUM_TESTS} + 1)); + + if [ "${DEBUG}" == '' ] || [ ${DEBUG} == ${NUM_TESTS} ]; then + local OPTIND; + local name= + local path= + local expectedCode= + local expectedResponseRegex= + local extraCurlOpts= + local scheme='http' + local port=${PORT} + local curlCommand= + local exitCode= + local response= + local testNum="${GRAY}${NUM_TESTS}${NC}\t" + + while getopts "n:asp:r:c:x:" option; do + case $option in + n) + name=$OPTARG;; + s) + scheme='https' + port=${SSL_PORT};; + p) + path=$OPTARG;; + c) + expectedCode=$OPTARG;; + r) + expectedResponseRegex=$OPTARG;; + x) + extraCurlOpts=$OPTARG;; + \?) # Invalid option + printf "Error: Invalid option\n"; + exit;; + esac + done + + curlCommand="curl -skv ${scheme}://nginx:${port}${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" + response=$(eval "${curlCommand}") + exitCode=$? + + printf "\n${testNum}" + + if [ "${exitCode}" -ne "0" ]; then + printf "${RED}${name} -- unexpected exit code from cURL\n\tcURL Exit Code: ${exitCode}"; + NUM_FAILED=$((${NUM_FAILED} + 1)); + else + local okay=1 + + if [ "${expectedCode}" != "" ]; then + local responseCode=$(echo "${response}" | grep -Eo 'HTTP/1.1 ([0-9]{3})' | awk '{print $2}') + + if [ "${expectedCode}" != "${responseCode}" ]; then + printf "${RED}${name} -- unexpected status code\n\tExpected: ${expectedCode}\n\tActual: ${responseCode}\n\tPath: ${path}" + NUM_FAILED=$((${NUM_FAILED} + 1)) + okay=0 + fi + fi + + if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ ${expectedResponseRegex} ]]; then + printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex//%/%%}" + NUM_FAILED=$((${NUM_FAILED} + 1)) + okay=0 + fi + + if [ "${okay}" == '1' ]; then + printf "${GREEN}${name}"; + fi + fi + + if [ "${DEBUG}" == "${NUM_TESTS}" ]; then + printf '\n\tcURL Command: %s' "${curlCommand:---}" + printf '\n\tResponse: %s' "${response:---}" + fi + + printf "${NC}\n" + else + NUM_SKIPPED=$((${NUM_SKIPPED} + 1)) + fi +} + +main() { + local JWT_HS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.r8tG8IZheiQ-i6HqUYyJj9V6dipgcQ4ZIdxau6QCZDo + local JWT_HS256_MISSING_SUB=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q + local JWT_HS256_MISSING_EMAIL=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM + local JWT_HS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.SS57j7PEybjbsp3g5W-IhhJHBmG5K-97qvgBKL16xj9ey-uMeEenWjGbB2vVp0kq + local JWT_HS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.xtSU6EWN2LILVsYzJFJpKnRkqjn_3qjz-J2ttNKnhZ60_5YjFeC8io4k8k1u77zlohSWvWMdugD9ZaB3vjJo-w + local JWT_RS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ + local JWT_RS256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 + local JWT_RS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.H35bTcZRhepWIoa8pKCbUMRuAOkVX9K5hJjc6tPmQwWmTw8lrktsvmMzJg_rgqnJLnAkciSIQw5EDj7fngS5zX2ThyRxrkPuE2Uiyw2Ect-mo9Kg1lrWgnyZCuCgq-Up9HQRAv0160mePlm8Gs4TOY6CPr38zwTcDZsy_Keq93igDQV8WuuWAGICaGd5ZyUOPjjzGShRjTU8Szz7fnpZpTtYRCYVo0pc5yfRWYm0fdn-4AseyGvd8JJ2xfnAEe4kZOkz7X1MLKtL0slKg3m2PH1lD7HwxIawXRTPWxArhJ9dcTNiDUrqtde2juGwOuMD_zTsb2Jj0_rmRb0Q6aljNw + local JWT_RS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.iUupyKypfXJ5aZWfItSW-mOmx9a4C4X7Yr5p5Fk8W75ZhkOq0EeNfstTxx870brhkdPovBhO2LYI44_HoH9XicQNL6JnFprE0r61eJFngbuzlhRQiWpq0xYrazJWc9zB7_GgL2ZCwtw-Ts3G23Q0632wVm6-d7MKvG7RS8aEjN-MuVGdtLglH3forpItmFxw-if40EQsBL7hncN_XNcQTO4KPHkqmlpac_oKXRrLFDIIt2tB6OOpvY4QcpERoxexp4pi2f-JoINnWX_dU5JnIs3ypVJLQPfoJvxg8fsg3zYrOvMYnfsqOCYoHtZGK0O7jyfFmcGo5v2hLT-CpoF3Zw + local JWT_ES256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.WFfJXGr5whKHB7arjsTXPTJ6TAsS1LoRxu7Vj2_HrLaIQphWJM6BICf-M3cv52tFzt-XTZb6GxlDgAbHo8z9Zg + local JWT_ES256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 + local JWT_ES384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._EFxXYOTAfT3gB3xUfgGR2UyXHeRTlDWqA94oZbB0DDa7YPZTEX9T4C_0ylnOFKZ6irGHZA8vxjgXDH3DZKWwBWcZ-XaQ_Q4Ws2J-AEeLqcl7_CS6q9mFo0Y7vUNEn-W + local JWT_ES512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.AFY4gNCtZNYkrTiijDkV4eKIt2UPMIuJBfZIk69jgI8FSGCQyUIMmIVg0fTvbaSiaryXzcjbG5TCm8a9Vu3KFJutAHGrgvZqcdklxx6Fbk3an3r_CH68n_ncwS3SUV58mDjf0OX8jRuNdudU1L5xYNQdodo-fxPIb1oHXfMJ0CmULDR9 + + run_test -n 'when auth disabled, should return 200' \ + -p '/' \ + -c '200' + + run_test -n '[SSL] when auth disabled, should return 200' \ + -s \ + -p '/' \ + -c '200' + + run_test -n 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + -p '/secure/auth-header/default' \ + -c '302' + + run_test -n '[SSL] when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + -s \ + -p '/secure/auth-header/default' \ + -c '302' + + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 200' \ + -p '/secure/auth-header/default/no-redirect' \ + -c '200' \ + -x "--header \"Authorization: ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header with Bearer, should return 200' \ + -p '/secure/auth-header/default/no-redirect' \ + -c '200' \ + -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with Authorization header with Bearer, should keep header intact' \ + -p '/secure/auth-header/default/proxy-header' \ + -c '200' \ + -r "< Test-Authorization: Bearer ${JWT_HS256_VALID}" \ + -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with Authorization header with Bearer, lower-case "bearer" should be accepted' \ + -p '/secure/auth-header/default/proxy-header' \ + -c '200' \ + -r "< Test-Authorization: bearer ${JWT_HS256_VALID}" \ + -x "--header \"Authorization: bearer ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ + -p '/secure/cookie/default' \ + -c '302' + + run_test -n 'when auth enabled with default algorithm with no redirect and no JWT cookie, should return 401' \ + -p '/secure/cookie/default/no-redirect' \ + -c '401' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x "--cookie jwt=${JWT_HS256_VALID}" + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub when sub validated, returns 302' \ + -p '/secure/cookie/default/validate-sub' \ + -c '302' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no email, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs256' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS384 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs384' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS384_VALID}"' + + run_test -n 'when auth enabled with HS512 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs512' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS512_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/rs256' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es256' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES256_VALID}"' + + run_test -n 'when auth enabled with ES384 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es384' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES384_VALID}"' + + run_test -n 'when auth enabled with ES512 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es512' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES512_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs256/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + -p '/secure/auth-header/rs256/file' \ + -c '302' \ + -x '--header "Authorization: Bearer ${JWT_RS256_INVALID}"' + + run_test -n 'when auth enabled with RS384 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs384/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS512 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs512/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es256/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + -p '/secure/auth-header/es256/file' \ + -c '302' \ + -x '--header "Authorization: Bearer ${JWT_ES256_INVALID}"' + + run_test -n 'when auth enabled with ES384 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es384/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES384_VALID}"' + + run_test -n 'when auth enabled with ES512 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es512/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES512_VALID}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header without bearer, returns 200' \ + -p '/secure/custom-header/hs256/' \ + -c '200' \ + -x '--header "Auth-Token: ${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header with bearer, returns 200' \ + -p '/secure/custom-header/hs256/' \ + -c '200' \ + -x '--header "Auth-Token: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to request variable' \ + -p '/secure/extract-claim/request/sub' \ + -r '< Test: sub=some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to request variable' \ + -p '/secure/extract-claim/request/name-1' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to request variable' \ + -p '/secure/extract-claim/request/name-2' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to request variable' \ + -p '/secure/extract-claim/request/nested' \ + -r '< Test: username=hello\.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to response variable' \ + -p '/secure/extract-claim/response/sub' \ + -r '< Test: sub=some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to response variable' \ + -p '/secure/extract-claim/response/name-1' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to response variable' \ + -p '/secure/extract-claim/response/name-2' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to response variable' \ + -p '/secure/extract-claim/response/nested' \ + -r '< Test: username=hello.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to response header' \ + -p '/secure/extract-claim/response/sub' \ + -r '< JWT-sub: some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to response header' \ + -p '/secure/extract-claim/response/name-1' \ + -r '< JWT-firstName: hello' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to response header' \ + -p '/secure/extract-claim/response/name-2' \ + -r '< JWT-firstName: hello' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to response header' \ + -p '/secure/extract-claim/response/nested' \ + -r '< JWT-username: hello\.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'tests single claim with if statement' \ + -p '/secure/extract-claim/if/sub' \ + -c 200 \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'tests absence of single claim with if statement' \ + -p '/secure/extract-claim/if/sub' \ + -c 401 \ + -x '--header "Authorization: Bearer ${JWT_HS256_MISSING_SUB}"' + + run_test -n 'extracts single claim to response body' \ + -p '/secure/extract-claim/body/sub' \ + -c 200 \ + -r 'sub: some-long-uuid$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims to response body' \ + -p '/secure/extract-claim/body/multiple' \ + -c 200 \ + -r 'you are: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'redirect based on claim' \ + -p '/profile/me' \ + -c 301 \ + -r '< Location: http://nginx:8000/profile/some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'returns 302 if auth enabled and no JWT provided' \ + -p '/return-url' \ + -c '302' + + run_test -n 'redirects to login if auth enabled and no JWT provided' \ + -p '/return-url' \ + -r '< Location: https://example\.com/login.*' + + run_test -n 'adds return_url to login URL when redirected to login' \ + -p '/return-url' \ + -r '< Location: https://example\.com/login\?return_url=http://nginx.*' + + run_test -n 'return_url includes port when redirected to login' \ + -p '/return-url' \ + -r "< Location: https://example\.com/login\?return_url=http://nginx:${PORT}/return-url" + + run_test -n 'return_url includes query when redirected to login' \ + -p '/return-url?test=123' \ + -r '< Location: https://example\.com/login\?return_url=http://nginx.*/return-url%3Ftest=123' + + if [[ "${NUM_FAILED}" = '0' ]]; then + printf "\nRan ${NUM_TESTS} tests successfully (skipped ${NUM_SKIPPED}).\n" + return 0 + else + printf "\nRan ${NUM_TESTS} tests: ${GREEN}$((${NUM_TESTS} - ${NUM_FAILED})) passed${NC}; ${RED}${NUM_FAILED} failed${NC}; ${NUM_SKIPPED} skipped\n" + return 1 + fi +} + +if [ "${DEBUG}" != '' ]; then + printf "\n${RED}Some tests will be skipped since DEBUG is set.${NC}\n" +fi + +printf "\n${GRAY}Starting tests using port ${PORT}...${NC}\n" +main From 7085f5c91503a92861797a266724be5bf2f3df5f Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 18 Feb 2025 11:23:46 -0500 Subject: [PATCH 125/130] update example config --- README.md | 2 +- {resources => examples}/nginx.conf | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) rename {resources => examples}/nginx.conf (70%) diff --git a/README.md b/README.md index f6b09d6..0fb2ea8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt ## Directives -This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. +This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. See the [example NGINX config file](examples/nginx.conf) for more info. | Directive | Description | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/resources/nginx.conf b/examples/nginx.conf similarity index 70% rename from resources/nginx.conf rename to examples/nginx.conf index 9b8feab..9795363 100644 --- a/resources/nginx.conf +++ b/examples/nginx.conf @@ -18,6 +18,15 @@ http { '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; + auth_jwt_enabled on; + auth_jwt_algorithm 'put_algo_here'; + auth_jwt_key 'put_key_here'; + auth_jwt_location 'COOKIE=auth-token'; + auth_jwt_redirect on; + auth_jwt_loginurl 'put_login_url_here'; + + # Include other auth_jwt_* directives as needed. + sendfile on; keepalive_timeout 65; From a110d0c34924d6af8b8602c37b64e41ee1bd2910 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 18 Feb 2025 12:35:01 -0500 Subject: [PATCH 126/130] update NGINX versions to build against --- .github/workflows/make-releases.yml | 9 ++++++++- scripts | 19 ++++++++++--------- test/test-nginx.dockerfile | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 487c4bf..d285bd8 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -30,7 +30,14 @@ jobs: strategy: matrix: # NGINX versions to build/test against - nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.26.2', '1.27.3'] + nginx-version: + - '1.20.2' # legacy + - '1.22.1' # legacy + - '1.24.0' # legacy + - '1.26.2' # stable + - '1.26.3' # stable + - '1.27.3' # mainline + - '1.27.4' # mainline # The following versions of libjwt are compatible: # * v1.0 - v1.12.0 diff --git a/scripts b/scripts index 59fc5c3..0d80ed5 100755 --- a/scripts +++ b/scripts @@ -19,17 +19,18 @@ SSL_IMAGE_MAP[$SSL_VERSION_3_0_11]="bookworm-slim:openssl-${SSL_VERSION_3_0_11}" SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" # supported NGINX versions -- for binary distribution -NGINX_VERSION_LEGACY_1='1.20.2' -NGINX_VERSION_LEGACY_2='1.22.1' -NGINX_VERSION_LEGACY_3='1.24.0' -NGINX_VERSION_STABLE='1.26.2' -NGINX_VERSION_MAINLINE='1.27.3' -NGINX_VERSIONS=(${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2} ${NGINX_VERSION_LEGACY_3} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_MAINLINE}) -NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} - +NGINX_VERSIONS=( + '1.20.2' # legacy + '1.22.1' # legacy + '1.24.0' # legacy + '1.26.2' # stable + '1.26.3' # stable + '1.27.3' # mainline + '1.27.4' # mainline +) +NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSIONS[-1]}} IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} FULL_IMAGE_NAME=${ORG_NAME:-teslagov}/${IMAGE_NAME} - TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" TEST_COMPOSE_FILE='test/docker-compose-test.yml' diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile index 1065558..f8c323f 100644 --- a/test/test-nginx.dockerfile +++ b/test/test-nginx.dockerfile @@ -1,6 +1,6 @@ ARG BASE_IMAGE -FROM ${BASE_IMAGE:?required} AS NGINX +FROM ${BASE_IMAGE:?required} ARG PORT ARG SSL_PORT From 8e4f2af67c5bfd91bb0820d63443cf8a6a33bd48 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 18 Feb 2025 12:48:54 -0500 Subject: [PATCH 127/130] update array style --- .github/workflows/make-releases.yml | 19 +++++++++++-------- scripts | 17 ++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index d285bd8..ab0aa87 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -31,13 +31,13 @@ jobs: matrix: # NGINX versions to build/test against nginx-version: - - '1.20.2' # legacy - - '1.22.1' # legacy - - '1.24.0' # legacy - - '1.26.2' # stable - - '1.26.3' # stable - - '1.27.3' # mainline - - '1.27.4' # mainline + - 1.20.2 # legacy + - 1.22.1 # legacy + - 1.24.0 # legacy + - 1.26.2 # stable + - 1.26.3 # stable + - 1.27.3 # mainline + - 1.27.4 # mainline # The following versions of libjwt are compatible: # * v1.0 - v1.12.0 @@ -47,7 +47,10 @@ jobs: # * Debian and Ubuntu's repos have v1.10.2 # * EPEL has v1.12.1 # This compiles against each version prior to a breaking change and the latest release - libjwt-version: ['1.12.0', '1.14.0', '1.15.3'] + libjwt-version: + - 1.12.0 + - 1.14.0 + - 1.15.3 runs-on: ubuntu-latest steps: diff --git a/scripts b/scripts index 0d80ed5..7ed7557 100755 --- a/scripts +++ b/scripts @@ -20,13 +20,13 @@ SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" # supported NGINX versions -- for binary distribution NGINX_VERSIONS=( - '1.20.2' # legacy - '1.22.1' # legacy - '1.24.0' # legacy - '1.26.2' # stable - '1.26.3' # stable - '1.27.3' # mainline - '1.27.4' # mainline + 1.20.2 # legacy + 1.22.1 # legacy + 1.24.0 # legacy + 1.26.2 # stable + 1.26.3 # stable + 1.27.3 # mainline + 1.27.4 # mainline ) NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSIONS[-1]}} IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} @@ -139,8 +139,7 @@ make_release() { -C bin/usr/lib64/nginx/modules ngx_http_auth_jwt_module.so > /dev/null } -# Create releases for the current mainline and stable version, as well as the 2 most recent "legacy" versions. -# See: https://nginx.org/en/download.html +# Create releases for all NGINX versions defined in `NGINX_VERSIONS`. make_releases() { local moduleVersion=$(git describe --tags --abbrev=0) From df75972a5554790aac25899e6ec654a8154bd995 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 18 Feb 2025 13:43:23 -0500 Subject: [PATCH 128/130] fix script function calls --- scripts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts b/scripts index 7ed7557..943bebc 100755 --- a/scripts +++ b/scripts @@ -103,7 +103,7 @@ cp_bin() { local stopContainer=0; if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME} | true)" != "true" ]; then - start_nginx + start stopContainer=1 fi @@ -117,7 +117,7 @@ cp_bin() { if [ $stopContainer ]; then printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" - stop_nginx + stop fi } From e019e2039ce9d1f6abb369ed8aa7179aaba95012 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 18 Feb 2025 17:25:06 -0500 Subject: [PATCH 129/130] clean up & fix build process --- .github/workflows/make-releases.yml | 23 +++--- nginx.dockerfile | 96 +++++++++++++++++-------- scripts | 106 +++++++++++++++++----------- test/test-runner.dockerfile | 2 +- 4 files changed, 143 insertions(+), 84 deletions(-) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index ab0aa87..c77edcb 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -29,7 +29,6 @@ jobs: needs: meta strategy: matrix: - # NGINX versions to build/test against nginx-version: - 1.20.2 # legacy - 1.22.1 # legacy @@ -39,14 +38,6 @@ jobs: - 1.27.3 # mainline - 1.27.4 # mainline - # The following versions of libjwt are compatible: - # * v1.0 - v1.12.0 - # * v1.12.1 - v1.14.0 - # * v1.15.0+ - # At the time of writing this: - # * Debian and Ubuntu's repos have v1.10.2 - # * EPEL has v1.12.1 - # This compiles against each version prior to a breaking change and the latest release libjwt-version: - 1.12.0 - 1.14.0 @@ -80,9 +71,10 @@ jobs: - name: Build jansson working-directory: ./jansson run: | - cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ - make && \ - make check && \ + set -e + cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF + make + make check sudo make install # TODO cache the build result so we don't have to do this every time? @@ -96,9 +88,10 @@ jobs: - name: Build libjwt working-directory: ./libjwt run: | - autoreconf -i && \ - ./configure && \ - make all && \ + set -e + autoreconf -i + ./configure + make all sudo make install - name: Download NGINX diff --git a/nginx.dockerfile b/nginx.dockerfile index 360469b..68c67f0 100644 --- a/nginx.dockerfile +++ b/nginx.dockerfile @@ -1,24 +1,42 @@ -ARG BASE_IMAGE +ARG BASE_IMAGE=${:?required} ARG NGINX_VERSION +ARG LIBJWT_VERSION -FROM ${BASE_IMAGE} AS ngx_http_auth_jwt_builder_base +FROM ${BASE_IMAGE} AS ngx_http_auth_jwt_builder LABEL stage=ngx_http_auth_jwt_builder -RUN chmod 1777 /tmp +ENV PATH="${PATH}:/etc/nginx" +ENV LD_LIBRARY_PATH=/usr/local/lib +ARG NGINX_VERSION +ARG LIBJWT_VERSION + RUN <<` -apt-get update -apt-get install -y curl build-essential + set -e + apt-get update + apt-get upgrade -y ` -FROM ngx_http_auth_jwt_builder_base AS ngx_http_auth_jwt_builder_module -LABEL stage=ngx_http_auth_jwt_builder -ENV PATH "${PATH}:/etc/nginx" -ENV LD_LIBRARY_PATH=/usr/local/lib -ARG NGINX_VERSION +RUN apt-get install -y curl git zlib1g-dev libpcre3-dev build-essential libpcre2-dev zlib1g-dev libpcre3-dev pkg-config cmake dh-autoreconf + +WORKDIR /root/build/libjansson RUN <<` set -e - apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev - mkdir -p /root/build/ngx-http-auth-jwt-module + git clone --depth 1 --branch v2.14 https://github.com/akheron/jansson . + cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF + make + make check + make install ` + +WORKDIR /root/build/libjwt +RUN <<` + set -e + git clone --depth 1 --branch v${LIBJWT_VERSION} https://github.com/benmcollins/libjwt . + autoreconf -i + ./configure + make all + make install +` + WORKDIR /root/build/ngx-http-auth-jwt-module ADD config ./ ADD src/*.h src/*.c ./src/ @@ -29,6 +47,7 @@ RUN <<` curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx ` + WORKDIR /root/build/nginx RUN <<` set -e @@ -89,30 +108,46 @@ RUN <<` ${BUILD_FLAGS} # --with-openssl=/usr/local \ ` + RUN make modules RUN make install -WORKDIR /usr/lib64/nginx/modules -RUN cp /root/build/nginx/objs/ngx_http_auth_jwt_module.so . + +WORKDIR /usr/lib/nginx/modules +RUN mv /root/build/nginx/objs/ngx_http_auth_jwt_module.so . RUN rm -rf /root/build -RUN adduser --system --no-create-home --shell /bin/false --group --disabled-login nginx -RUN mkdir -p /var/cache/nginx /var/log/nginx -WORKDIR /etc/nginx -FROM ngx_http_auth_jwt_builder_module AS ngx_http_auth_jwt_nginx -LABEL maintainer="TeslaGov" email="developers@teslagov.com" -ARG NGINX_VERSION RUN <<` set -e - - apt-get update - apt-get install -y libjansson4 libjwt0 + apt-get remove -y curl git zlib1g-dev libpcre3-dev build-essential libpcre2-dev zlib1g-dev libpcre3-dev pkg-config cmake dh-autoreconf + # apt-get install -y gnupg2 ca-certificates lsb-release debian-archive-keyring apt-get clean ` + +RUN <<` + set -e + groupadd nginx + useradd -g nginx nginx +` + +# RUN <<` +# set -e +# curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor > /usr/share/keyrings/nginx-archive-keyring.gpg +# printf "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian `lsb_release -cs` nginx\n" > /etc/apt/sources.list.d/nginx.list +# printf "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" > /etc/apt/preferences.d/99nginx +# ` + +# RUN <<` +# set -e +# apt-get update +# apt-get install -y nginx +# ` + COPY <<` /etc/nginx/nginx.conf +daemon off; user nginx; pid /var/run/nginx.pid; -load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; +load_module /usr/lib/nginx/modules/ngx_http_auth_jwt_module.so; worker_processes 1; @@ -124,12 +159,17 @@ http { include mime.types; default_type application/octet-stream; - log_format main '$$remote_addr - $$remote_user [$$time_local] "$$request" ' - '$$status $$body_bytes_sent "$$http_referer" ' - '"$$http_user_agent" "$$http_x_forwarded_for"'; + log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' + '\$status \$body_bytes_sent "\$http_referer" ' + '"\$http_user_agent" "\$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; include conf.d/*.conf; } ` -ENTRYPOINT ["nginx", "-g", "daemon off;"] + +WORKDIR /var/cache/nginx +RUN chown nginx:nginx . + +WORKDIR / +CMD ["nginx"] diff --git a/scripts b/scripts index 943bebc..ca48984 100755 --- a/scripts +++ b/scripts @@ -8,14 +8,17 @@ NC='\033[0m' # supported SSL versions SSL_VERSION_1_1_1w='1.1.1w' -SSL_VERSION_3_0_11='3.0.11' +SSL_VERSION_3_0_15='3.0.15' SSL_VERSION_3_2_1='3.2.1' -SSL_VERSIONS=(${SSL_VERSION_3_2_1}) -SSL_VERSION=${SSL_VERSION:-$SSL_VERSION_3_0_11} +SSL_VERSIONS=( + ${SSL_VERSION_1_1_1w} + ${SSL_VERSION_3_0_15} + ${SSL_VERSION_3_2_1} +) declare -A SSL_IMAGE_MAP SSL_IMAGE_MAP[$SSL_VERSION_1_1_1w]="bullseye-slim:openssl-${SSL_VERSION_1_1_1w}" -SSL_IMAGE_MAP[$SSL_VERSION_3_0_11]="bookworm-slim:openssl-${SSL_VERSION_3_0_11}" +SSL_IMAGE_MAP[$SSL_VERSION_3_0_15]="bookworm-slim:openssl-${SSL_VERSION_3_0_15}" SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" # supported NGINX versions -- for binary distribution @@ -28,7 +31,27 @@ NGINX_VERSIONS=( 1.27.3 # mainline 1.27.4 # mainline ) + +# The following versions of libjwt are compatible: +# * v1.0 - v1.12.0 +# * v1.12.1 - v1.14.0 +# * v1.15.0+ +# At the time of writing this: +# * Debian and Ubuntu's repos have v1.10.2 +# * EPEL has v1.12.1 +# This compiles against each version prior to a breaking change and the latest release +LIBJWT_VERSION_DEBIAN=1.12.0 +LIBJWT_VERSION_EPEL=1.14.0 +LIBJWT_VERSION_LATEST=1.15.3 +LIBJWT_VERSIONS=( + ${LIBJWT_VERSION_DEBIAN} + ${LIBJWT_VERSION_EPEL} + ${LIBJWT_VERSION_LATEST} +) + +SSL_VERSION=${SSL_VERSION:-$SSL_VERSION_3_0_15} NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSIONS[-1]}} +LIBJWT_VERSION=${LIBJWT_VERSION:-${LIBJWT_VERSION_DEBIAN}} IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} FULL_IMAGE_NAME=${ORG_NAME:-teslagov}/${IMAGE_NAME} TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" @@ -40,7 +63,7 @@ all() { test_all } -verify_and_build_base_image() { +build_base_image() { local image=${SSL_IMAGE_MAP[$SSL_VERSION]} local baseImage=${image%%:*} @@ -53,23 +76,24 @@ verify_and_build_base_image() { --build-arg BASE_IMAGE=debian:${baseImage} \ --build-arg SSL_VERSION=${SSL_VERSION} \ -f openssl.dockerfile \ - -t ${image} . + -t ${image} \ + . fi } build_module() { - local dockerArgs=${1:-} local baseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} - - verify_and_build_base_image - printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}...${NC}\n" + build_base_image + + printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}, libjwt ${LIBJWT_VERSION}...${NC}\n" docker buildx build \ -f nginx.dockerfile \ -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ --build-arg BASE_IMAGE=${baseImage} \ --build-arg NGINX_VERSION=${NGINX_VERSION} \ - ${dockerArgs} . + --build-arg LIBJWT_VERSION=${LIBJWT_VERSION} \ + . if [ "$?" -ne 0 ]; then printf "${RED}✘ Build failed ${NC}\n" @@ -79,12 +103,9 @@ build_module() { } rebuild_module() { - clean_module - build_module --no-cache -} - -clean_module() { docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true + + build_module } start() { @@ -111,9 +132,9 @@ cp_bin() { rm -rf ${destDir}/* mkdir -p ${destDir} docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ - usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ - usr/lib/$(uname -m)-linux-gnu/libjansson.so.* \ - usr/lib/$(uname -m)-linux-gnu/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null + usr/lib/nginx/modules/ngx_http_auth_jwt_module.so \ + usr/local/lib/libjansson.so.* \ + usr/local/lib/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null if [ $stopContainer ]; then printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" @@ -122,31 +143,30 @@ cp_bin() { } make_release() { - local moduleVersion=${1} - - NGINX_VERSION=${2} + local moduleVersion=$(git describe --tags --abbrev=0) printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" rebuild_module rebuild_test - test + test --no-build cp_bin mkdir -p release - tar -czvf release/ngx_http_auth_jwt_module_${moduleVersion}_nginx_${NGINX_VERSION}.tgz \ + tar -czvf release/ngx-http-auth-jwt-module-${moduleVersion}_libjwt-${LIBJWT_VERSION}_nginx-${NGINX_VERSION}.tgz \ README.md \ - -C bin/usr/lib64/nginx/modules ngx_http_auth_jwt_module.so > /dev/null + -C bin/usr/lib/nginx/modules ngx_http_auth_jwt_module.so > /dev/null } # Create releases for all NGINX versions defined in `NGINX_VERSIONS`. make_releases() { - local moduleVersion=$(git describe --tags --abbrev=0) - rm -rf release/* - for v in ${NGINX_VERSIONS[@]}; do - make_release ${moduleVersion} ${v} + for NGINX_VERSION in ${NGINX_VERSIONS[@]}; do + for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do + export NGINX_VERSION LIBJWT_VERSION + make_release + done done } @@ -178,14 +198,21 @@ rebuild_test() { test_all() { for SSL_VERSION in "${SSL_VERSIONS[@]}"; do for NGINX_VERSION in "${NGINX_VERSIONS[@]}"; do - test + for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do + export SSL_VERSION NGINX_VERSION LIBJWT_VERSION + test + done done done } test() { - build_module - build_test + if [[ ! "$*" =~ --no-build ]]; then + build_module + build_test + fi + + trap 'test_cleanup' 0 printf "${MAGENTA}Running tests...${NC}\n" docker compose \ @@ -193,8 +220,6 @@ test() { -f ${TEST_COMPOSE_FILE} up \ --no-start - trap test_cleanup 0 - test_now } @@ -202,21 +227,22 @@ test_now() { nginxContainerName="${TEST_CONTAINER_NAME_PREFIX}-nginx" runnerContainerName="${TEST_CONTAINER_NAME_PREFIX}-runner" + echo + echo "Executing tests with the following options:" + echo " SSL Version: ${SSL_VERSION}" + echo " LIBJWT Version: ${LIBJWT_VERSION}" + echo " NGINX Version: ${NGINX_VERSION}" + docker start ${nginxContainerName} if [ "$(docker container inspect -f '{{.State.Running}}' ${nginxContainerName})" != "true" ]; then printf "${RED}Failed to start container \"${nginxContainerName}\". See logs below:\n" docker logs ${nginxContainerName} printf "${NC}\n" - return + return 1 fi docker start -a ${runnerContainerName} - - echo - echo "Tests were executed with the following options:" - echo " SSL Version: ${SSL_VERSION}" - echo " NGINX Version: ${NGINX_VERSION}" } test_cleanup() { diff --git a/test/test-runner.dockerfile b/test/test-runner.dockerfile index 18fc3d3..377c40e 100644 --- a/test/test-runner.dockerfile +++ b/test/test-runner.dockerfile @@ -15,4 +15,4 @@ RUN <<` COPY test.sh . -CMD ./test.sh +CMD ["./test.sh"] From b128954b6c3fc95d8538e75abac637405bdbd562 Mon Sep 17 00:00:00 2001 From: Josh McCullough Date: Tue, 29 Jul 2025 16:02:03 -0400 Subject: [PATCH 130/130] specify platform / fix spacing --- scripts | 336 ++++++++++++++++++----------------- test/docker-compose-test.yml | 8 +- 2 files changed, 176 insertions(+), 168 deletions(-) diff --git a/scripts b/scripts index ca48984..7ce7024 100755 --- a/scripts +++ b/scripts @@ -11,9 +11,9 @@ SSL_VERSION_1_1_1w='1.1.1w' SSL_VERSION_3_0_15='3.0.15' SSL_VERSION_3_2_1='3.2.1' SSL_VERSIONS=( - ${SSL_VERSION_1_1_1w} - ${SSL_VERSION_3_0_15} - ${SSL_VERSION_3_2_1} + ${SSL_VERSION_1_1_1w} + ${SSL_VERSION_3_0_15} + ${SSL_VERSION_3_2_1} ) declare -A SSL_IMAGE_MAP @@ -23,13 +23,13 @@ SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" # supported NGINX versions -- for binary distribution NGINX_VERSIONS=( - 1.20.2 # legacy - 1.22.1 # legacy - 1.24.0 # legacy - 1.26.2 # stable - 1.26.3 # stable - 1.27.3 # mainline - 1.27.4 # mainline + 1.20.2 # legacy + 1.22.1 # legacy + 1.24.0 # legacy + 1.26.2 # stable + 1.26.3 # stable + 1.27.3 # mainline + 1.27.4 # mainline ) # The following versions of libjwt are compatible: @@ -44,9 +44,9 @@ LIBJWT_VERSION_DEBIAN=1.12.0 LIBJWT_VERSION_EPEL=1.14.0 LIBJWT_VERSION_LATEST=1.15.3 LIBJWT_VERSIONS=( - ${LIBJWT_VERSION_DEBIAN} - ${LIBJWT_VERSION_EPEL} - ${LIBJWT_VERSION_LATEST} + ${LIBJWT_VERSION_DEBIAN} + ${LIBJWT_VERSION_EPEL} + ${LIBJWT_VERSION_LATEST} ) SSL_VERSION=${SSL_VERSION:-$SSL_VERSION_3_0_15} @@ -58,216 +58,218 @@ TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" TEST_COMPOSE_FILE='test/docker-compose-test.yml' all() { - build_module - build_test - test_all + build_module + build_test + test_all } build_base_image() { - local image=${SSL_IMAGE_MAP[$SSL_VERSION]} - local baseImage=${image%%:*} - - if [ -z ${image} ]; then - echo "Base image not set for SSL version :${SSL_VERSION}" - exit 1 - else - printf "${MAGENTA}Building ${baseImage} base image for SSL ${SSL_VERSION}...${NC}\n" - docker buildx build \ - --build-arg BASE_IMAGE=debian:${baseImage} \ - --build-arg SSL_VERSION=${SSL_VERSION} \ - -f openssl.dockerfile \ - -t ${image} \ - . - fi + local image=${SSL_IMAGE_MAP[$SSL_VERSION]} + local baseImage=${image%%:*} + + if [ -z ${image} ]; then + echo "Base image not set for SSL version :${SSL_VERSION}" + exit 1 + else + printf "${MAGENTA}Building ${baseImage} base image for SSL ${SSL_VERSION}...${NC}\n" + docker buildx build \ + --platform linux/amd64 \ + --build-arg BASE_IMAGE=debian:${baseImage} \ + --build-arg SSL_VERSION=${SSL_VERSION} \ + -f openssl.dockerfile \ + -t ${image} \ + . + fi } build_module() { - local baseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} - - build_base_image - - printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}, libjwt ${LIBJWT_VERSION}...${NC}\n" - docker buildx build \ - -f nginx.dockerfile \ - -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ - --build-arg BASE_IMAGE=${baseImage} \ - --build-arg NGINX_VERSION=${NGINX_VERSION} \ - --build-arg LIBJWT_VERSION=${LIBJWT_VERSION} \ - . - - if [ "$?" -ne 0 ]; then - printf "${RED}✘ Build failed ${NC}\n" - else - printf "${GREEN}✔ Successfully built NGINX module ${NC}\n" - fi + local baseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} + + build_base_image + + printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}, libjwt ${LIBJWT_VERSION}...${NC}\n" + docker buildx build \ + --platform linux/amd64 \ + -f nginx.dockerfile \ + -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ + --build-arg BASE_IMAGE=${baseImage} \ + --build-arg NGINX_VERSION=${NGINX_VERSION} \ + --build-arg LIBJWT_VERSION=${LIBJWT_VERSION} \ + . + + if [ "$?" -ne 0 ]; then + printf "${RED}✘ Build failed ${NC}\n" + else + printf "${GREEN}✔ Successfully built NGINX module ${NC}\n" + fi } rebuild_module() { - docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true + docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true - build_module + build_module } start() { - local port=$(get_port) + local port=$(get_port) - printf "${MAGENTA}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" - docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME}:${NGINX_VERSION} >/dev/null + printf "${MAGENTA}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" + docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME}:${NGINX_VERSION} >/dev/null } stop() { - docker stop "${IMAGE_NAME}" >/dev/null + docker stop "${IMAGE_NAME}" >/dev/null } cp_bin() { - local destDir=bin - local stopContainer=0; - - if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME} | true)" != "true" ]; then - start - stopContainer=1 - fi - - printf "${MAGENTA}Copying binaries to: ${destDir}${NC}\n" - rm -rf ${destDir}/* - mkdir -p ${destDir} - docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ - usr/lib/nginx/modules/ngx_http_auth_jwt_module.so \ - usr/local/lib/libjansson.so.* \ - usr/local/lib/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null - - if [ $stopContainer ]; then - printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" - stop - fi + local destDir=bin + local stopContainer=0; + + if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME} | true)" != "true" ]; then + start + stopContainer=1 + fi + + printf "${MAGENTA}Copying binaries to: ${destDir}${NC}\n" + rm -rf ${destDir}/* + mkdir -p ${destDir} + docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ + usr/lib/nginx/modules/ngx_http_auth_jwt_module.so \ + usr/local/lib/libjansson.so.* \ + usr/local/lib/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null + + if [ $stopContainer ]; then + printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" + stop + fi } make_release() { - local moduleVersion=$(git describe --tags --abbrev=0) + local moduleVersion=$(git describe --tags --abbrev=0) - printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" + printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" - rebuild_module - rebuild_test - test --no-build - cp_bin + rebuild_module + rebuild_test + test --no-build + cp_bin - mkdir -p release - tar -czvf release/ngx-http-auth-jwt-module-${moduleVersion}_libjwt-${LIBJWT_VERSION}_nginx-${NGINX_VERSION}.tgz \ - README.md \ - -C bin/usr/lib/nginx/modules ngx_http_auth_jwt_module.so > /dev/null + mkdir -p release + tar -czvf release/ngx-http-auth-jwt-module-${moduleVersion}_libjwt-${LIBJWT_VERSION}_nginx-${NGINX_VERSION}.tgz \ + README.md \ + -C bin/usr/lib/nginx/modules ngx_http_auth_jwt_module.so > /dev/null } # Create releases for all NGINX versions defined in `NGINX_VERSIONS`. make_releases() { - rm -rf release/* - - for NGINX_VERSION in ${NGINX_VERSIONS[@]}; do - for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do - export NGINX_VERSION LIBJWT_VERSION - make_release - done - done + rm -rf release/* + + for NGINX_VERSION in ${NGINX_VERSIONS[@]}; do + for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do + export NGINX_VERSION LIBJWT_VERSION + make_release + done + done } build_test() { - local dockerArgs=${1:-} - local port=$(get_port) - local sslPort=$(get_port $((port + 1))) - local runnerBaseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} - - export TEST_CONTAINER_NAME_PREFIX - export FULL_IMAGE_NAME - export NGINX_VERSION - - printf "${MAGENTA}Building test NGINX & runner using port ${port}...${NC}\n" - docker compose \ - -p ${TEST_CONTAINER_NAME_PREFIX} \ - -f ${TEST_COMPOSE_FILE} \ - build \ - --build-arg RUNNER_BASE_IMAGE=${runnerBaseImage} \ - --build-arg PORT=${port} \ - --build-arg SSL_PORT=${sslPort} \ - ${dockerArgs} + local dockerArgs=${1:-} + local port=$(get_port) + local sslPort=$(get_port $((port + 1))) + local runnerBaseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} + + export TEST_CONTAINER_NAME_PREFIX + export FULL_IMAGE_NAME + export NGINX_VERSION + + printf "${MAGENTA}Building test NGINX & runner using port ${port}...${NC}\n" + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} \ + build \ + --build-arg RUNNER_BASE_IMAGE=${runnerBaseImage} \ + --build-arg PORT=${port} \ + --build-arg SSL_PORT=${sslPort} \ + ${dockerArgs} } rebuild_test() { - build_test --no-cache + build_test --no-cache } test_all() { - for SSL_VERSION in "${SSL_VERSIONS[@]}"; do - for NGINX_VERSION in "${NGINX_VERSIONS[@]}"; do - for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do - export SSL_VERSION NGINX_VERSION LIBJWT_VERSION - test - done - done - done + for SSL_VERSION in "${SSL_VERSIONS[@]}"; do + for NGINX_VERSION in "${NGINX_VERSIONS[@]}"; do + for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do + export SSL_VERSION NGINX_VERSION LIBJWT_VERSION + test + done + done + done } test() { - if [[ ! "$*" =~ --no-build ]]; then - build_module - build_test - fi + if [[ ! "$*" =~ --no-build ]]; then + build_module + build_test + fi - trap 'test_cleanup' 0 + trap 'test_cleanup' 0 - printf "${MAGENTA}Running tests...${NC}\n" - docker compose \ - -p ${TEST_CONTAINER_NAME_PREFIX} \ - -f ${TEST_COMPOSE_FILE} up \ - --no-start + printf "${MAGENTA}Running tests...${NC}\n" + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} up \ + --no-start - test_now + test_now } test_now() { - nginxContainerName="${TEST_CONTAINER_NAME_PREFIX}-nginx" - runnerContainerName="${TEST_CONTAINER_NAME_PREFIX}-runner" - - echo - echo "Executing tests with the following options:" - echo " SSL Version: ${SSL_VERSION}" - echo " LIBJWT Version: ${LIBJWT_VERSION}" - echo " NGINX Version: ${NGINX_VERSION}" - - docker start ${nginxContainerName} - - if [ "$(docker container inspect -f '{{.State.Running}}' ${nginxContainerName})" != "true" ]; then - printf "${RED}Failed to start container \"${nginxContainerName}\". See logs below:\n" - docker logs ${nginxContainerName} - printf "${NC}\n" - return 1 - fi - - docker start -a ${runnerContainerName} + nginxContainerName="${TEST_CONTAINER_NAME_PREFIX}-nginx" + runnerContainerName="${TEST_CONTAINER_NAME_PREFIX}-runner" + + echo + echo "Executing tests with the following options:" + echo " SSL Version: ${SSL_VERSION}" + echo " LIBJWT Version: ${LIBJWT_VERSION}" + echo " NGINX Version: ${NGINX_VERSION}" + + docker start ${nginxContainerName} + + if [ "$(docker container inspect -f '{{.State.Running}}' ${nginxContainerName})" != "true" ]; then + printf "${RED}Failed to start container \"${nginxContainerName}\". See logs below:\n" + docker logs ${nginxContainerName} + printf "${NC}\n" + return 1 + fi + + docker start -a ${runnerContainerName} } test_cleanup() { - docker compose \ - -p ${TEST_CONTAINER_NAME_PREFIX} \ - -f ${TEST_COMPOSE_FILE} down + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} down } get_port() { - startPort=${1:-8000} - endPort=$((startPort + 100)) - - for p in $(seq ${startPort} ${endPort}); do - if ! ss -ln | grep -q ":${p} "; then - echo ${p} - break - fi - done + startPort=${1:-8000} + endPort=$((startPort + 100)) + + for p in $(seq ${startPort} ${endPort}); do + if ! ss -ln | grep -q ":${p} "; then + echo ${p} + break + fi + done } if [ $# -eq 0 ]; then - all + all else - fn=$1 - shift - - ${fn} "$@" + fn=$1 + shift + + ${fn} "$@" fi diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index 72ff710..cc570c5 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -5,8 +5,11 @@ services: build: context: . dockerfile: test-nginx.dockerfile + platforms: + - linux/amd64 args: BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} + platform: linux/amd64 logging: driver: ${LOG_DRIVER:-journald} @@ -15,5 +18,8 @@ services: build: context: . dockerfile: test-runner.dockerfile + platforms: + - linux/amd64 + platform: linux/amd64 depends_on: - - nginx \ No newline at end of file + - nginx