diff --git a/README.md b/README.md index 8c34d5c..2b62f97 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,12 @@ By default, the`Authorization` header is used to provide a JWT for validation. H ```nginx 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 +auth_jwt_location QUERYSTRING=token; # get the JWT from the "token" querystring parameter ``` +### Querystring Location +If you use the `QUERYSTRING` location, the given querystring parameter is stripped from the outgoing request, if the request is to be forwarded. Please also note that including a JWT in the querystring may be a security risk as the JWT could be logged/viewed by intermediary devices/software. In most cases it is best to store the JWT in a header. + ## `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: diff --git a/config b/config index 0271a79..b66bdd1 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/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_srcs="${ngx_addon_dir}/src/arrays.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_args_processing.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/ngx_http_auth_jwt_args_processing.c b/src/ngx_http_auth_jwt_args_processing.c new file mode 100644 index 0000000..feb9e68 --- /dev/null +++ b/src/ngx_http_auth_jwt_args_processing.c @@ -0,0 +1,83 @@ +#include "ngx_http_auth_jwt_args_processing.h" + +/* Creates a new version of args without token present. + * Writes length of new args to `*write_args_len`. + */ +u_char *create_args_without_token( + ngx_pool_t *pool, + ngx_str_t *args, + size_t token_key_start, + size_t token_end, + size_t *write_args_len +) { + *write_args_len = args->len - token_end + token_key_start; + u_char *args_ptr = ngx_palloc(pool, *write_args_len); + + if (args_ptr == NULL) + { + return NULL; + } + else + { + if (token_key_start > 0) { + ngx_memcpy(args_ptr, args->data, token_key_start); + } + if (token_end < (args->len - 1)) { + ngx_memcpy( + args_ptr + token_key_start, + args->data + token_end, + *write_args_len - token_key_start + ); + } + + return args_ptr; + } +} + +/* Tries to extract token from query string. Returns true if found, false otherwise. + + Searches for the string contained in *jwt_location in *args. If it finds the token + in question it writes the location of the start of key to *write_to_token_key_start, + start of token itself to *write_to_token_value_start and end of token to *write_to_token_end. +*/ +bool search_token_from_args( + const ngx_str_t *jwt_location, + const ngx_str_t *args, + size_t *write_to_token_key_start, + size_t *write_to_token_value_start, + size_t *write_to_token_end +) { + size_t i = 0, j = 0; + size_t max_i = args->len > jwt_location->len ? args->len - jwt_location->len : 0; + + while (i < max_i) + { + j = 0; + if (i == 0 || *(args->data + i - 1) == '&') + { + while (j < jwt_location->len && *(args->data + i + j) == *(jwt_location->data + j)) + { + if (j == (jwt_location->len - 1)) + { + *write_to_token_key_start = i; + i++; + if (i >= max_i || *(args->data + i + j) != '=') + { + // key doesn't match + break; + } + *write_to_token_value_start = i + j + 1; + while (i < args->len && *(args->data + i) != '&') + { + i++; + } + *write_to_token_end = i; + return true; + } + j++; + } + } + i++; + } + return false; +} diff --git a/src/ngx_http_auth_jwt_args_processing.h b/src/ngx_http_auth_jwt_args_processing.h new file mode 100644 index 0000000..f743479 --- /dev/null +++ b/src/ngx_http_auth_jwt_args_processing.h @@ -0,0 +1,23 @@ +#ifndef _NGX_HTTP_AUTH_JWT_ARGS_PROCESSING_H +#define _NGX_HTTP_AUTH_JWT_ARGS_PROCESSING_H + +#include +#include + +u_char *create_args_without_token( + ngx_pool_t *pool, + ngx_str_t *args, + size_t token_key_start, + size_t token_end, + size_t *write_mutated_args_len +); + +bool search_token_from_args( + const ngx_str_t *jwt_location, + const ngx_str_t *args, + size_t *write_to_token_key_start, + size_t *write_to_token_value_start, + size_t *write_to_token_end +); + +#endif /* _NGX_HTTP_AUTH_JWT_ARGS_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 744b50d..d392727 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -15,6 +15,7 @@ #include #include "arrays.h" +#include "ngx_http_auth_jwt_args_processing.h" #include "ngx_http_auth_jwt_header_processing.h" #include "ngx_http_auth_jwt_binary_converters.h" #include "ngx_http_auth_jwt_string.h" @@ -611,8 +612,9 @@ 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 *COOKIE_PREFIX = "COOKIE="; + static const char HEADER_PREFIX[] = "HEADER="; + static const char COOKIE_PREFIX[] = "COOKIE="; + static const char QUERY_STRING_PREFIX[] = "QUERYSTRING="; char *jwtPtr = NULL; ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "jwt_location.len %d", jwt_location.len); @@ -670,6 +672,57 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); } } + else if (jwt_location.len > sizeof(QUERY_STRING_PREFIX) && ngx_strncmp(jwt_location.data, QUERY_STRING_PREFIX, sizeof(QUERY_STRING_PREFIX) - 1) == 0) { + jwt_location.data += sizeof(QUERY_STRING_PREFIX) - 1; + jwt_location.len -= sizeof(QUERY_STRING_PREFIX) - 1; + size_t token_key_start = 0, token_value_start = 0, token_end = 0; + + bool found_token = search_token_from_args( + &jwt_location, + &r->args, + &token_key_start, + &token_value_start, + &token_end + ); + + if (found_token) + { + int token_len = token_end - token_value_start; + + jwtPtr = ngx_palloc(r->pool, token_len + 1); + if (jwtPtr != NULL) { + ngx_memcpy(jwtPtr, r->args.data + token_value_start, token_len); + *(jwtPtr + token_len) = '\0'; + } + + // strip first or last & from args + if (token_key_start > 0) + { + token_key_start--; + } + else if (token_end < (r->args.len - 1)) + { + token_end++; + } + + size_t mutated_args_len = 0; + + // Strip key from args + u_char *args_ptr = create_args_without_token( + r->pool, + &r->args, + token_key_start, + token_end, + &mutated_args_len + ); + + if (args_ptr != NULL) + { + r->args.data = args_ptr; + r->args.len = mutated_args_len; + } + } + } return jwtPtr; } diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 00c990b..0315675 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -165,6 +165,15 @@ BwIDAQAB try_files index.html =404; } + location /secure/auth-query-string/hs256 { + auth_jwt_enabled on; + auth_jwt_location QUERYSTRING=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; diff --git a/test/test.sh b/test/test.sh index 29a0bf3..f2ea0fe 100755 --- a/test/test.sh +++ b/test/test.sh @@ -202,6 +202,50 @@ main() { -p '/secure/custom-header/hs256/' \ -c '200' \ -x '--header "Auth-Token: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT as query-string jwt-token, returns 200' \ + -p '/secure/auth-query-string/hs256/?token=${JWT_HS256_VALID}' \ + -c '200' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT as query-string something-jwt-token, returns 401' \ + -p '/secure/auth-query-string/hs256/?something-jwt-token=${JWT_HS256_VALID}' \ + -c '401' + + run_test -n 'when auth enabled with HS256 algorithm and non-valid JWT as query-string jwt-token, returns 401' \ + -p '/secure/auth-query-string/hs256/?token=ABBAKEY' \ + -c '401' + + run_test -n 'when auth enabled with HS256 algorithm and no query-string present, returns 401' \ + -p '/secure/auth-query-string/hs256/' \ + -c '401' + + run_test -n 'when auth enabled with HS256 algorithm and empty token present, returns 401' \ + -p '/secure/auth-query-string/hs256/?token' \ + -c '401' + + run_test -n 'when auth enabled with HS256 algorithm and empty token present, returns 401' \ + -p '/secure/auth-query-string/hs256/?token=' \ + -c '401' + + run_test -n 'when auth enabled with HS256 algorithm and just random query-string present, returns 401' \ + -p '/secure/auth-query-string/hs256/?key1=abba' \ + -c '401' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT as query-string jwt-token after first query-string, returns 200' \ + -p '/secure/auth-query-string/hs256/?key1=abba\&token=${JWT_HS256_VALID}' \ + -c '200' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT as query-string jwt-token, second query-string after, returns 200' \ + -p '/secure/auth-query-string/hs256/?token=${JWT_HS256_VALID}\&key1=abba' \ + -c '200' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT as query-string jwt-token in middle, returns 200' \ + -p '/secure/auth-query-string/hs256/?key1=abba\&token=${JWT_HS256_VALID}\&key2=abba' \ + -c '200' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT as query-string jwt-token with overlapping query-string first, returns 200' \ + -p '/secure/auth-query-string/hs256/?jwt-token2=abba\&token=${JWT_HS256_VALID}' \ + -c '200' run_test -n 'extracts single claim to request variable' \ -p '/secure/extract-claim/request/sub' \