-
Notifications
You must be signed in to change notification settings - Fork 5.4k
jwt_authn: implementation of www-authenticate header #16216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
d298dac
668491b
2aa8dd9
6d2118a
12d2c8e
f2a80f5
b5681e9
5d1dfd9
25e4248
e2bed72
191c61c
7ee776f
84a311b
a7f4854
25c19f1
6b326ed
4ee3863
89c8cb0
d2dd536
ef47927
0b2c1a2
c9d5541
e4f1d82
5bed8e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -751,6 +751,23 @@ const std::string& Utility::getProtocolString(const Protocol protocol) { | |||
| NOT_REACHED_GCOVR_EXCL_LINE; | ||||
| } | ||||
|
|
||||
| const std::string Utility::buildOriginalUri(const Http::RequestHeaderMap& request_headers, | ||||
| const uint32_t max_path_length) { | ||||
| if (!request_headers.Path()) { | ||||
| return ""; | ||||
| } | ||||
| absl::string_view path(request_headers.EnvoyOriginalPath() | ||||
| ? request_headers.getEnvoyOriginalPathValue() | ||||
| : request_headers.getPathValue()); | ||||
|
|
||||
| if (path.length() > max_path_length) { | ||||
| path = path.substr(0, max_path_length); | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens in practice if we truncate the path? Will consumers handle this gracefully? Maybe we should increment some stat at least to indicate that this is happening?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So specifically, this code is based on how the
I'm fine with simplifying it if we understand the reduction in functionality we get by removing the extra checks. However, since this is in the common library and while this filter uses it in a way that the getEnvoyOriginalPath() check is not necessary, it may be necessary in the general case. Truncating the path would return an "incomplete" URI. There's no defined length limit for URIs in the HTTP standard, though in practice most browser will balk at anything over 2048 characters. Consumers could set an arbitrarily high max_path_length or perhaps a flag could be added to say, I do not want truncation, if it matters to consumers. I do agree it would be nice to surface the fact that the truncation happened,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're copying the implementation from another place to put it into common code, can we have that other location also use this shared code? The jwt filter will never see the original host header at the time this is called due to the call to buildUrl being in decodeHeaders, but if this is being used by other things I agree we should be incorporating the original host header (and ideally document this behavior as part of the docstring for this function). How about we make the trunaction flag optional (absl::optional<uint32_t>) and don't do truncation in this case unless required? Then the tracer lib can call this and preserve the trunacation behavior there while this new usage doesn't truncate until we know that this is necessary
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh and I would love to see unit tests testing this helper directly instead of via extensions, e.g. something in test/common/http/utility_test
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree on all points! I'll be working on this today |
||||
| } | ||||
|
|
||||
| return absl::StrCat(request_headers.getForwardedProtoValue(), "://", | ||||
|
lizan marked this conversation as resolved.
|
||||
| request_headers.getHostValue(), path); | ||||
| } | ||||
|
|
||||
| void Utility::extractHostPathFromUri(const absl::string_view& uri, absl::string_view& host, | ||||
| absl::string_view& path) { | ||||
| /** | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,8 @@ namespace HttpFilters { | |
| namespace JwtAuthn { | ||
|
|
||
| namespace { | ||
|
|
||
| const absl::string_view InvalidTokenErrorString = "invalid_token"; | ||
| const uint32_t MaximumUriLength = 256; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| Http::RegisterCustomInlineHeader<Http::CustomInlineHeaderRegistry::Type::RequestHeaders> | ||
| access_control_request_method_handle(Http::CustomHeaders::get().AccessControlRequestMethod); | ||
| Http::RegisterCustomInlineHeader<Http::CustomInlineHeaderRegistry::Type::RequestHeaders> | ||
|
|
@@ -87,6 +88,7 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, | |
| if (verifier == nullptr) { | ||
| onComplete(Status::Ok); | ||
| } else { | ||
| original_uri_ = Http::Utility::buildOriginalUri(headers, MaximumUriLength); | ||
| // Verify the JWT token, onComplete() will be called when completed. | ||
| context_ = Verifier::createContext(headers, decoder_callbacks_->activeSpan(), this); | ||
| verifier->verify(context_); | ||
|
|
@@ -119,8 +121,15 @@ void Filter::onComplete(const Status& status) { | |
| status == Status::JwtAudienceNotAllowed ? Http::Code::Forbidden : Http::Code::Unauthorized; | ||
| // return failure reason as message body | ||
| decoder_callbacks_->sendLocalReply( | ||
| code, ::google::jwt_verify::getStatusString(status), nullptr, absl::nullopt, | ||
| generateRcDetails(::google::jwt_verify::getStatusString(status))); | ||
| code, ::google::jwt_verify::getStatusString(status), | ||
| [uri = this->original_uri_, status](Http::ResponseHeaderMap& headers) { | ||
| std::string value = absl::StrCat("Bearer realm=\"", uri, "\""); | ||
| if (status != Status::JwtMissed) { | ||
| absl::StrAppend(&value, ", error=\"", InvalidTokenErrorString, "\""); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe make
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do that, but we'll still need to append a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, scratch that. I see what you mean now. |
||
| } | ||
| headers.setCopy(Http::Headers::get().WWWAuthenticate, value); | ||
| }, | ||
| absl::nullopt, generateRcDetails(::google::jwt_verify::getStatusString(status))); | ||
| return; | ||
| } | ||
| stats_.allowed_.inc(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ namespace JwtAuthn { | |
| namespace { | ||
|
|
||
| const char HeaderToFilterStateFilterName[] = "envoy.filters.http.header_to_filter_state_for_test"; | ||
|
|
||
| const Http::LowerCaseString WwwAuthenticateHeader("www-authenticate"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will lead static-initialization fiasco hence flaky test, use a function with CONSTRUCT_ON_FIRST_USE.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Swapped it over to the same value we use in the filter itself |
||
| // This filter extracts a string header from "header" and | ||
| // save it into FilterState as name "state" as read-only Router::StringAccessor. | ||
| class HeaderToFilterStateFilter : public Http::PassThroughDecoderFilter { | ||
|
|
@@ -140,6 +140,9 @@ TEST_P(LocalJwksIntegrationTest, ExpiredToken) { | |
| ASSERT_TRUE(response->waitForEndStream()); | ||
| ASSERT_TRUE(response->complete()); | ||
| EXPECT_EQ("401", response->headers().getStatusValue()); | ||
| EXPECT_EQ(1, response->headers().get(WwwAuthenticateHeader).size()); | ||
| EXPECT_EQ("Bearer realm=\"http://host/\", error=\"invalid_token\"", | ||
| response->headers().get(WwwAuthenticateHeader)[0]->value().getStringView()); | ||
| } | ||
|
|
||
| TEST_P(LocalJwksIntegrationTest, MissingToken) { | ||
|
|
@@ -158,6 +161,8 @@ TEST_P(LocalJwksIntegrationTest, MissingToken) { | |
| ASSERT_TRUE(response->waitForEndStream()); | ||
| ASSERT_TRUE(response->complete()); | ||
| EXPECT_EQ("401", response->headers().getStatusValue()); | ||
| EXPECT_EQ("Bearer realm=\"http://host/\"", | ||
| response->headers().get(WwwAuthenticateHeader)[0]->value().getStringView()); | ||
| } | ||
|
|
||
| TEST_P(LocalJwksIntegrationTest, ExpiredTokenHeadReply) { | ||
|
|
@@ -177,6 +182,9 @@ TEST_P(LocalJwksIntegrationTest, ExpiredTokenHeadReply) { | |
| ASSERT_TRUE(response->waitForEndStream()); | ||
| ASSERT_TRUE(response->complete()); | ||
| EXPECT_EQ("401", response->headers().getStatusValue()); | ||
| EXPECT_EQ("Bearer realm=\"http://host/\", error=\"invalid_token\"", | ||
| response->headers().get(WwwAuthenticateHeader)[0]->value().getStringView()); | ||
|
|
||
| EXPECT_NE("0", response->headers().getContentLengthValue()); | ||
| EXPECT_THAT(response->body(), ::testing::IsEmpty()); | ||
| } | ||
|
|
@@ -424,6 +432,8 @@ TEST_P(RemoteJwksIntegrationTest, FetchFailedJwks) { | |
| ASSERT_TRUE(response->waitForEndStream()); | ||
| ASSERT_TRUE(response->complete()); | ||
| EXPECT_EQ("401", response->headers().getStatusValue()); | ||
| EXPECT_EQ("Bearer realm=\"http://host/\", error=\"invalid_token\"", | ||
| response->headers().get(WwwAuthenticateHeader)[0]->value().getStringView()); | ||
|
|
||
| cleanup(); | ||
| } | ||
|
|
@@ -444,7 +454,8 @@ TEST_P(RemoteJwksIntegrationTest, FetchFailedMissingCluster) { | |
| ASSERT_TRUE(response->waitForEndStream()); | ||
| ASSERT_TRUE(response->complete()); | ||
| EXPECT_EQ("401", response->headers().getStatusValue()); | ||
|
|
||
| EXPECT_EQ("Bearer realm=\"http://host/\", error=\"invalid_token\"", | ||
| response->headers().get(WwwAuthenticateHeader)[0]->value().getStringView()); | ||
| cleanup(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will we have case "envoy-origin-path" exists but not "path"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears in the router config_impl, that if
envoy-origin-pathexists, it is set to thepathvalue. So no, there should not be any possibility of envoy-origin-path existing but path not existing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That code in the router_config runs after headers make it to the router, so I don't think this will have been set at this point. On the other hand I think the check for Path is unnecessary because
getPathValue()should return an empty string in that case. Digging into it some more, original-path is only set in for certain route actions, so there might not be a point in reading it at this point at all?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping on this, I think we can simplify this logic