Skip to content

Commit e5d9cb2

Browse files
authored
Send the client secret in a header where the provider takes one (#1171)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 6cf741b commit e5d9cb2

17 files changed

Lines changed: 282 additions & 64 deletions

File tree

DEPENDENCIES

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02
22
uwebsockets https://github.com/uNetworking/uWebSockets v20.79.0
3-
core https://github.com/sourcemeta/core 1ffe47bdda541e13733d61116739f5a8b6092eea
3+
core https://github.com/sourcemeta/core 8aff35edd5285002de0ac87a2c06fc4bca4b4d5d
44
blaze https://github.com/sourcemeta/blaze ca1949507ea5f4215f9a55ca796cd074602ff705
55
jsonbinpack https://github.com/sourcemeta/jsonbinpack f775b2df5fa89d5a70acb940a5c938173811fea7
66
jsonschema https://github.com/sourcemeta/jsonschema v16.3.0

enterprise/authentication/authentication.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,15 @@ struct Authentication::Impl {
957957
resolved.end_session = document.value().end_session_endpoint().value();
958958
}
959959

960+
// RFC 6749 Section 2.3.1 requires every server to accept the client
961+
// secret in an authorization header and discourages carrying it in the
962+
// request body, so the body is used only where the header is refused.
963+
// A provider that lists nothing is taken to accept the header, which is
964+
// what the specification assigns to saying nothing
965+
resolved.token_endpoint_basic_auth =
966+
document.value().supports_token_endpoint_auth_method(
967+
"client_secret_basic");
968+
960969
cached.source = server;
961970
cached.resolved = std::move(resolved);
962971
}

enterprise/server/include/sourcemeta/one/enterprise_server_action_auth_callback_v1.h

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ class ActionAuthCallback_v1 : public sourcemeta::one::RouterAction {
199199

200200
const auto id_token{this->exchange(
201201
endpoints.value().token, policy->client_id, client_secret.value(),
202-
redirect_uri, code, verifier->to_string())};
202+
redirect_uri, code, verifier->to_string(),
203+
endpoints.value().token_endpoint_basic_auth)};
203204
if (!id_token.has_value()) {
204205
this->fail(request, response, sourcemeta::core::HTTP_STATUS_BAD_GATEWAY,
205206
"urn:sourcemeta:one:auth-exchange-failed",
@@ -449,13 +450,17 @@ class ActionAuthCallback_v1 : public sourcemeta::one::RouterAction {
449450
}
450451
}
451452

452-
[[nodiscard]] auto exchange(const std::string_view token_endpoint,
453-
const std::string_view client_id,
454-
const std::string_view client_secret,
455-
const std::string_view redirect_uri,
456-
const std::string_view code,
457-
const std::string_view code_verifier) const
458-
-> std::optional<std::string> {
453+
// RFC 6749 Section 2.3.1 has every server accept the client secret in an
454+
// authorization header, and asks that carrying it in the request body be
455+
// limited to clients that cannot send one. A body is the part of a request
456+
// that logging and proxies keep, while an authorization header is the part
457+
// they already know to redact, so the header is used wherever the provider
458+
// takes it
459+
[[nodiscard]] auto exchange(
460+
const std::string_view token_endpoint, const std::string_view client_id,
461+
const std::string_view client_secret, const std::string_view redirect_uri,
462+
const std::string_view code, const std::string_view code_verifier,
463+
const bool basic_auth) const -> std::optional<std::string> {
459464
try {
460465
sourcemeta::core::HTTPSystemRequest fetch{
461466
std::string{token_endpoint}, sourcemeta::core::HTTPMethod::POST};
@@ -466,8 +471,16 @@ class ActionAuthCallback_v1 : public sourcemeta::one::RouterAction {
466471
sourcemeta::core::SecureString body;
467472
sourcemeta::core::oauth_build_token_request_code(code, redirect_uri,
468473
code_verifier, {}, body);
469-
sourcemeta::core::oauth_client_secret_post(client_id, client_secret,
470-
body);
474+
if (basic_auth) {
475+
sourcemeta::core::SecureString authorization;
476+
sourcemeta::core::oauth_client_secret_basic(client_id, client_secret,
477+
authorization);
478+
fetch.header("authorization", std::move(authorization));
479+
} else {
480+
sourcemeta::core::oauth_client_secret_post(client_id, client_secret,
481+
body);
482+
}
483+
471484
fetch.body(std::move(body), "application/x-www-form-urlencoded");
472485
const auto result{fetch.send()};
473486
if (result.status.code < 200 || result.status.code >= 300) {

enterprise/unit/authentication/authentication_test.cc

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2174,6 +2174,110 @@ TEST(provider_endpoints_are_retrieved_once_and_reused) {
21742174
EXPECT_EQ(*calls, 1);
21752175
}
21762176

2177+
TEST(a_provider_naming_no_authentication_method_takes_the_header) {
2178+
const std::array<std::string_view, 1> paths{{"/portal"}};
2179+
const std::array<sourcemeta::one::Authentication::Policy, 1> policies{
2180+
{{.paths = paths,
2181+
.type = sourcemeta::one::Authentication::Type::OIDC,
2182+
.issuer = "https://login.test",
2183+
.client_id = "client",
2184+
.client_secret_variable = "ONE_TEST_OIDC_AUTH_ABSENT",
2185+
.name = "okta",
2186+
.session_secret_variable = SESSION_SECRET_VARIABLE}}};
2187+
const auto path{test_path("oidc_auth_absent.bin")};
2188+
sourcemeta::one::Authentication::save(policies, path, path, anywhere);
2189+
2190+
const std::map<std::string, std::string> responses{
2191+
{"https://login.test/.well-known/openid-configuration",
2192+
R"JSON({
2193+
"issuer": "https://login.test",
2194+
"authorization_endpoint": "https://login.test/authorize",
2195+
"token_endpoint": "https://login.test/token",
2196+
"jwks_uri": "https://login.test/jwks",
2197+
2198+
"response_types_supported": [ "code" ],
2199+
"subject_types_supported": [ "public" ],
2200+
"id_token_signing_alg_values_supported": [ "RS256" ]
2201+
})JSON"}};
2202+
const sourcemeta::one::Authentication authentication{
2203+
path, stub_fetcher(responses, nullptr)};
2204+
2205+
const auto endpoints{authentication.endpoints("okta")};
2206+
EXPECT_TRUE(endpoints.has_value());
2207+
// RFC 8414 Section 2 makes an absent list mean `client_secret_basic`, so
2208+
// saying nothing is an answer rather than the absence of one
2209+
EXPECT_TRUE(endpoints.value().token_endpoint_basic_auth);
2210+
}
2211+
2212+
TEST(a_provider_naming_the_header_takes_the_header) {
2213+
const std::array<std::string_view, 1> paths{{"/portal"}};
2214+
const std::array<sourcemeta::one::Authentication::Policy, 1> policies{
2215+
{{.paths = paths,
2216+
.type = sourcemeta::one::Authentication::Type::OIDC,
2217+
.issuer = "https://login.test",
2218+
.client_id = "client",
2219+
.client_secret_variable = "ONE_TEST_OIDC_AUTH_BASIC",
2220+
.name = "okta",
2221+
.session_secret_variable = SESSION_SECRET_VARIABLE}}};
2222+
const auto path{test_path("oidc_auth_basic.bin")};
2223+
sourcemeta::one::Authentication::save(policies, path, path, anywhere);
2224+
2225+
const std::map<std::string, std::string> responses{
2226+
{"https://login.test/.well-known/openid-configuration",
2227+
R"JSON({
2228+
"issuer": "https://login.test",
2229+
"authorization_endpoint": "https://login.test/authorize",
2230+
"token_endpoint": "https://login.test/token",
2231+
"jwks_uri": "https://login.test/jwks",
2232+
"token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ],
2233+
"response_types_supported": [ "code" ],
2234+
"subject_types_supported": [ "public" ],
2235+
"id_token_signing_alg_values_supported": [ "RS256" ]
2236+
})JSON"}};
2237+
const sourcemeta::one::Authentication authentication{
2238+
path, stub_fetcher(responses, nullptr)};
2239+
2240+
const auto endpoints{authentication.endpoints("okta")};
2241+
EXPECT_TRUE(endpoints.has_value());
2242+
// Offering both, the header is the one RFC 6749 Section 2.3.1 asks for
2243+
EXPECT_TRUE(endpoints.value().token_endpoint_basic_auth);
2244+
}
2245+
2246+
TEST(a_provider_refusing_the_header_gets_the_body_instead) {
2247+
const std::array<std::string_view, 1> paths{{"/portal"}};
2248+
const std::array<sourcemeta::one::Authentication::Policy, 1> policies{
2249+
{{.paths = paths,
2250+
.type = sourcemeta::one::Authentication::Type::OIDC,
2251+
.issuer = "https://login.test",
2252+
.client_id = "client",
2253+
.client_secret_variable = "ONE_TEST_OIDC_AUTH_POST",
2254+
.name = "okta",
2255+
.session_secret_variable = SESSION_SECRET_VARIABLE}}};
2256+
const auto path{test_path("oidc_auth_post.bin")};
2257+
sourcemeta::one::Authentication::save(policies, path, path, anywhere);
2258+
2259+
const std::map<std::string, std::string> responses{
2260+
{"https://login.test/.well-known/openid-configuration",
2261+
R"JSON({
2262+
"issuer": "https://login.test",
2263+
"authorization_endpoint": "https://login.test/authorize",
2264+
"token_endpoint": "https://login.test/token",
2265+
"jwks_uri": "https://login.test/jwks",
2266+
"token_endpoint_auth_methods_supported": [ "client_secret_post" ],
2267+
"response_types_supported": [ "code" ],
2268+
"subject_types_supported": [ "public" ],
2269+
"id_token_signing_alg_values_supported": [ "RS256" ]
2270+
})JSON"}};
2271+
const sourcemeta::one::Authentication authentication{
2272+
path, stub_fetcher(responses, nullptr)};
2273+
2274+
const auto endpoints{authentication.endpoints("okta")};
2275+
EXPECT_TRUE(endpoints.has_value());
2276+
// A provider that does not take the header leaves the body as the only way
2277+
// to authenticate, so the preference gives way rather than the login failing
2278+
EXPECT_FALSE(endpoints.value().token_endpoint_basic_auth);
2279+
}
2280+
21772281
TEST(provider_endpoints_of_an_unreachable_provider_are_absent) {
21782282
const auto calls{std::make_shared<int>(0)};
21792283
const std::array<std::string_view, 1> paths{{"/portal"}};

src/authentication/include/sourcemeta/one/authentication.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ class SOURCEMETA_ONE_AUTHENTICATION_EXPORT Authentication {
212212
std::string jwks_uri{};
213213
// Absent from a provider that does not offer to end its own session
214214
std::string end_session{};
215+
// Whether the provider takes the client secret in an authorization header
216+
// rather than in the request body
217+
bool token_endpoint_basic_auth{true};
215218
};
216219

217220
// What the named interactive policy's provider says about itself, retrieved

vendor/core/src/core/http/aws_sigv4.cc

Lines changed: 20 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/core/src/core/http/client_curl.cc

Lines changed: 22 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/core/src/core/http/client_darwin.mm

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)