Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
67857d0
Issue #6355: jwt_authn extracts JWT more flexibly
larrywest Mar 22, 2019
5e026dc
Issue 6355: run 'tools/check_format.py fix' on extractor.cc
larrywest Mar 26, 2019
c75bb4e
Issue 6355: style: k -> Constant, comments
larrywest Mar 26, 2019
5457729
Issue 6355: fix style problems in test
larrywest Mar 26, 2019
631ef16
Issue #6355: auto-fix style in test
larrywest Mar 26, 2019
8add0e5
Issue #6355: restore #include accidentally removed 2 commits ago
larrywest Mar 26, 2019
af5ce5c
Issue #6355: pedantic 'spelling' fixes in comments.
larrywest Mar 26, 2019
52ff835
Issue #6355: auto-fix of header inclusion ordering
larrywest Mar 26, 2019
42cbc4a
Issue 6355: per qizwhang review comment, remove...
larrywest Mar 26, 2019
6ad1680
Issue 6355: update api/.../jwt_authn/.../README.md
larrywest Mar 26, 2019
394fae4
Issue #6355: empty commit to trigger CircleCI
larrywest Mar 27, 2019
cea55d3
Issue #6355: another empty commit to trigger CircleCI
larrywest Mar 27, 2019
07b2976
Issue #6355: change input param from const& to const
larrywest Mar 27, 2019
1ea97c9
Issue #6355: change test object initialization
larrywest Mar 27, 2019
e5f735b
Issue #6355: auto-fix style
larrywest Mar 27, 2019
152e124
Issue #6355: change input param from const to const&
larrywest Mar 28, 2019
7f7ff1b
Issue #6355: pass string_view as recommended
larrywest Apr 1, 2019
87bc5c2
Issue #6355: empty commit to trigger re-build
larrywest Apr 1, 2019
4b89509
Mention enhancement of jwt_authn filter (PR#6384)
larrywest Apr 3, 2019
a3110c7
Fix to pass check_format.py
larrywest Apr 3, 2019
f9ee217
Merge branch 'master' of https://github.com/envoyproxy/envoy into iss…
larrywest Apr 3, 2019
8f94bf7
Issue #6355: change version_history line to "jwt_authn:"
larrywest Apr 3, 2019
d752369
Issue #6355: remove 4 duplicated lines
larrywest Apr 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions api/envoy/config/filter/http/jwt_authn/v2alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,38 @@ If a custom location is desired, `from_headers` or `from_params` can be used to
## HTTP header to pass successfully verified JWT

If a JWT is valid, its payload will be passed to the backend in a new HTTP header specified in `forward_payload_header` field. Its value is base64 encoded JWT payload in JSON.


## Further header options

In addition to the `name` field, which specifies the HTTP header name,
the `from_headers` section can specify an optional `value_prefix` value, as in:

```yaml
from_headers:
- name: bespoke
value_prefix: jwt_value
```

The above will cause the jwt_authn filter to look for the JWT in the `bespoke` header, following the tag `jwt_value`.

Any non-JWT characters (i.e., anything _other than_ alphanumerics, `_`, `-`, and `.`) will be skipped,
and all following, contiguous, JWT-legal chars will be taken as the JWT.

This means all of the following will return a JWT of `eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk`:

```text
bespoke: jwt_value=eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk

bespoke: {"jwt_value": "eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk"}

bespoke: beta:true,jwt_value:"eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk",trace=1234
```

The header `name` may be `Authorization`.

The `value_prefix` must match exactly, i.e., case-sensitively.
If the `value_prefix` is not found, the header is skipped: not considered as a source for a JWT token.

If there are no JWT-legal characters after the `value_prefix`, the entire string after it
is taken to be the JWT token. This is unlikely to succeed; the error will reported by the JWT parser.
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Version history
* http: added :ref:`max request headers size <envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.max_request_headers_kb>`. The default behaviour is unchanged.
* http: added modifyDecodingBuffer/modifyEncodingBuffer to allow modifying the buffered request/response data.
* http: added encodeComplete/decodeComplete. These are invoked at the end of the stream, after all data has been encoded/decoded respectively. Default implementation is a no-op.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the other lines are irrelevant, can you merge master?

Also start the jwt line with jwt_authn: , and sort please.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, line 82 has tracing: in the midst of a few upstream: lines.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah thanks, we will fix that during the release process.

* jwt_authn: make filter's parsing of JWT more flexible, allowing syntax like ``jwt=eyJhbGciOiJS...ZFnFIw,extra=7,realm=123``
* outlier_detection: added support for :ref:`outlier detection event protobuf-based logging <arch_overview_outlier_detection_logging>`.
* mysql: added a MySQL proxy filter that is capable of parsing SQL queries over MySQL wire protocol. Refer to :ref:`MySQL proxy<config_network_filters_mysql_proxy>` for more details.
* performance: new buffer implementation (disabled by default; to test it, add "--use-libevent-buffers 0" to the command-line arguments when starting Envoy).
Expand Down
41 changes: 38 additions & 3 deletions source/extensions/filters/http/jwt_authn/extractor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class ExtractorImpl : public Extractor {
// ctor helper for a jwt provider config
void addProvider(const JwtProvider& provider);

// @return what should be the 3-part base64url-encoded substring; see RFC-7519
absl::string_view extractJWT(absl::string_view value_str,
absl::string_view::size_type after) const;

// HeaderMap value type to store prefix and issuers that specified this
// header.
struct HeaderLocationSpec {
Expand Down Expand Up @@ -181,11 +185,12 @@ std::vector<JwtLocationConstPtr> ExtractorImpl::extract(const Http::HeaderMap& h
if (entry) {
auto value_str = entry->value().getStringView();
if (!location_spec->value_prefix_.empty()) {
if (!absl::StartsWith(value_str, location_spec->value_prefix_)) {
// prefix doesn't match, skip it.
const auto pos = value_str.find(location_spec->value_prefix_);
if (pos == absl::string_view::npos) {
// value_prefix not found anywhere in value_str, so skip
continue;
}
value_str = value_str.substr(location_spec->value_prefix_.size());
value_str = extractJWT(value_str, pos + location_spec->value_prefix_.length());
}
tokens.push_back(std::make_unique<const JwtHeaderLocation>(
std::string(value_str), location_spec->specified_issuers_, location_spec->header_));
Expand All @@ -211,6 +216,36 @@ std::vector<JwtLocationConstPtr> ExtractorImpl::extract(const Http::HeaderMap& h
return tokens;
}

// as specified in RFC-4648 § 5, plus dot (period, 0x2e), of which two are required in the JWT
constexpr absl::string_view ConstantBase64UrlEncodingCharsPlusDot =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";

// Returns a token, not a URL: skips non-Base64Url-legal (or dot) characters, collects following
// Base64Url+dot string until first non-Base64Url char.
//
// The input parameters:
// "value_str" - the header value string, perhaps "Bearer string....", and
// "after" - the offset into that string after which to begin looking for JWT-legal characters
//
// For backwards compatibility, if it finds no suitable string, it returns value_str as-is.
//
// It is forgiving w.r.t. dots/periods, as the exact syntax will be verified after extraction.
//
// See RFC-7519 § 2, RFC-7515 § 2, and RFC-4648 "Base-N Encodings" § 5.
absl::string_view ExtractorImpl::extractJWT(absl::string_view value_str,
absl::string_view::size_type after) const {
const auto starting = value_str.find_first_of(ConstantBase64UrlEncodingCharsPlusDot, after);
if (starting == value_str.npos) {
return value_str;
}
// There should be two dots (periods; 0x2e) inside the string, but we don't verify that here
auto ending = value_str.find_first_not_of(ConstantBase64UrlEncodingCharsPlusDot, starting);
if (ending == value_str.npos) { // Base64Url-encoded string occupies the rest of the line
return value_str.substr(starting);
}
return value_str.substr(starting, ending - starting);
}

void ExtractorImpl::sanitizePayloadHeaders(Http::HeaderMap& headers) const {
for (const auto& header : forward_payload_headers_) {
headers.remove(header);
Expand Down
1 change: 1 addition & 0 deletions test/extensions/filters/http/jwt_authn/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ envoy_extension_cc_test(
extension_name = "envoy.filters.http.jwt_authn",
deps = [
"//source/extensions/filters/http/jwt_authn:extractor_lib",
"//test/extensions/filters/http/jwt_authn:test_common_lib",
"//test/test_common:utility_lib",
],
)
Expand Down
62 changes: 62 additions & 0 deletions test/extensions/filters/http/jwt_authn/extractor_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "extensions/filters/http/jwt_authn/extractor.h"

#include "test/extensions/filters/http/jwt_authn/test_common.h"
#include "test/test_common/utility.h"

using ::envoy::config::filter::http::jwt_authn::v2alpha::JwtAuthentication;
Expand Down Expand Up @@ -46,6 +47,16 @@ const char ExampleConfig[] = R"(
from_headers:
- name: prefix-header
value_prefix: AAABBB
provider7:
issuer: issuer7
from_headers:
- name: prefix-header
value_prefix: CCCDDD
provider8:
issuer: issuer8
from_headers:
- name: prefix-header
value_prefix: '"CCCDDD"'
)";

class ExtractorTest : public testing::Test {
Expand Down Expand Up @@ -102,6 +113,19 @@ TEST_F(ExtractorTest, TestDefaultHeaderLocation) {
EXPECT_FALSE(headers.Authorization());
}

// Test extracting JWT as Bearer token from the default header location: "Authorization" -
// using an actual (correctly-formatted) JWT:
TEST_F(ExtractorTest, TestDefaultHeaderLocationWithValidJWT) {
auto headers =
TestHeaderMapImpl{{absl::StrCat("Authorization"), absl::StrCat("Bearer ", GoodToken)}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 1);

// Only the issue1 is using default header location.
EXPECT_EQ(tokens[0]->token(), GoodToken);
EXPECT_TRUE(tokens[0]->isIssuerSpecified("issuer1"));
}

// Test extracting token from the default query parameter: "access_token"
TEST_F(ExtractorTest, TestDefaultParamLocation) {
auto headers = TestHeaderMapImpl{{":path", "/path?access_token=jwt_token"}};
Expand Down Expand Up @@ -172,6 +196,44 @@ TEST_F(ExtractorTest, TestPrefixHeaderMatch) {
EXPECT_FALSE(headers.get(Http::LowerCaseString("prefix-header")));
}

// Test extracting token from the custom header: "prefix-header"
// The value is found after the "CCCDDD", then between the '=' and the ','.
TEST_F(ExtractorTest, TestPrefixHeaderFlexibleMatch1) {
auto headers = TestHeaderMapImpl{{"prefix-header", "preamble CCCDDD=jwt_token,extra=more"}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 1);

// Match issuer 7 with map key as: prefix-header + 'CCCDDD'
EXPECT_TRUE(tokens[0]->isIssuerSpecified("issuer7"));
EXPECT_EQ(tokens[0]->token(), "jwt_token");
}

TEST_F(ExtractorTest, TestPrefixHeaderFlexibleMatch2) {
auto headers =
TestHeaderMapImpl{{"prefix-header", "CCCDDD=\"and0X3Rva2Vu\",comment=\"fish tag\""}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 1);

// Match issuer 7 with map key as: prefix-header + AAA
EXPECT_TRUE(tokens[0]->isIssuerSpecified("issuer7"));
EXPECT_EQ(tokens[0]->token(), "and0X3Rva2Vu");
}

TEST_F(ExtractorTest, TestPrefixHeaderFlexibleMatch3) {
auto headers = TestHeaderMapImpl{
{"prefix-header", "creds={\"authLevel\": \"20\", \"CCCDDD\": \"and0X3Rva2Vu\"}"}};
auto tokens = extractor_->extract(headers);
EXPECT_EQ(tokens.size(), 2);

// Match issuer 8 with map key as: prefix-header + '"CCCDDD"'
EXPECT_TRUE(tokens[0]->isIssuerSpecified("issuer8"));
EXPECT_EQ(tokens[0]->token(), "and0X3Rva2Vu");

// Match issuer 7 with map key as: prefix-header + 'CCCDDD'
EXPECT_TRUE(tokens[1]->isIssuerSpecified("issuer7"));
EXPECT_EQ(tokens[1]->token(), "and0X3Rva2Vu");
}

// Test extracting token from the custom query parameter: "token_param"
TEST_F(ExtractorTest, TestCustomParamToken) {
auto headers = TestHeaderMapImpl{{":path", "/path?token_param=jwt_token"}};
Expand Down