You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by vi...@apache.org on 2017/03/13 15:46:30 UTC
[4/6] mesos git commit: Implemented the JWT authenticator.
Implemented the JWT authenticator.
This HTTP authenticator extracts a JWT from the requests' authorization
header using the 'Bearer' schema and validates it against a secret using
HMAC SHA256. The 'sub' claim of the JWT is the extracted principal, all
other claims will be additional labels of the 'Principal'.
Review: https://reviews.apache.org/r/56753/
Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/b4a16030
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/b4a16030
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/b4a16030
Branch: refs/heads/master
Commit: b4a16030ffc000323cdcd2d22f9c29d4e6c061d0
Parents: 51140af
Author: Jan Schlicht <ja...@mesosphere.io>
Authored: Mon Mar 13 16:46:03 2017 +0100
Committer: Vinod Kone <vi...@gmail.com>
Committed: Mon Mar 13 16:46:03 2017 +0100
----------------------------------------------------------------------
3rdparty/libprocess/Makefile.am | 1 +
.../include/process/authenticator.hpp | 38 ++++-
3rdparty/libprocess/src/jwt_authenticator.cpp | 167 +++++++++++++++++++
3rdparty/libprocess/src/tests/http_tests.cpp | 87 ++++++++++
4 files changed, 290 insertions(+), 3 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mesos/blob/b4a16030/3rdparty/libprocess/Makefile.am
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/Makefile.am b/3rdparty/libprocess/Makefile.am
index 18bcabb..80758d6 100644
--- a/3rdparty/libprocess/Makefile.am
+++ b/3rdparty/libprocess/Makefile.am
@@ -182,6 +182,7 @@ libprocess_la_SOURCES = \
if ENABLE_SSL
libprocess_la_SOURCES += \
src/jwt.cpp \
+ src/jwt_authenticator.cpp \
src/libevent_ssl_socket.cpp \
src/libevent_ssl_socket.hpp \
src/openssl.cpp \
http://git-wip-us.apache.org/repos/asf/mesos/blob/b4a16030/3rdparty/libprocess/include/process/authenticator.hpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/include/process/authenticator.hpp b/3rdparty/libprocess/include/process/authenticator.hpp
index 7ab41e9..272d921 100644
--- a/3rdparty/libprocess/include/process/authenticator.hpp
+++ b/3rdparty/libprocess/include/process/authenticator.hpp
@@ -27,6 +27,9 @@ namespace http {
namespace authentication {
class BasicAuthenticatorProcess;
+#ifdef USE_SSL_SOCKET
+class JWTAuthenticatorProcess;
+#endif // USE_SSL_SOCKET
/**
* Contains information associated with an authenticated principal.
@@ -135,17 +138,46 @@ public:
const std::string& realm,
const hashmap<std::string, std::string>& credentials);
- virtual ~BasicAuthenticator();
+ ~BasicAuthenticator() override;
- virtual Future<AuthenticationResult> authenticate(
+ Future<AuthenticationResult> authenticate(
const http::Request& request) override;
- virtual std::string scheme() const override;
+ std::string scheme() const override;
private:
Owned<BasicAuthenticatorProcess> process_;
};
+
+#ifdef USE_SSL_SOCKET
+
+/**
+ * Implements the "Bearer" authentication scheme using JSON Web Tokens.
+ *
+ * The authenticator uses a JWT implementation that is compliant with
+ * RFC 7519, validating 'HS256' signed tokens using the specified
+ * secret key. If the authentication was successful, the claims of the
+ * returned principal will be set to the claims within the token's
+ * payload.
+ */
+class JWTAuthenticator : public Authenticator
+{
+public:
+ JWTAuthenticator(const std::string& realm, const std::string& secret);
+
+ ~JWTAuthenticator() override;
+
+ Future<AuthenticationResult> authenticate(
+ const http::Request& request) override;
+
+ std::string scheme() const override;
+
+private:
+ Owned<JWTAuthenticatorProcess> process_;
+};
+#endif // USE_SSL_SOCKET
+
} // namespace authentication {
} // namespace http {
} // namespace process {
http://git-wip-us.apache.org/repos/asf/mesos/blob/b4a16030/3rdparty/libprocess/src/jwt_authenticator.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/src/jwt_authenticator.cpp b/3rdparty/libprocess/src/jwt_authenticator.cpp
new file mode 100644
index 0000000..e402447
--- /dev/null
+++ b/3rdparty/libprocess/src/jwt_authenticator.cpp
@@ -0,0 +1,167 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License
+
+#include <process/authenticator.hpp>
+
+#include <map>
+#include <vector>
+
+#include <process/dispatch.hpp>
+#include <process/http.hpp>
+#include <process/id.hpp>
+#include <process/jwt.hpp>
+#include <process/process.hpp>
+
+#include <stout/foreach.hpp>
+#include <stout/try.hpp>
+#include <stout/unreachable.hpp>
+
+namespace process {
+namespace http {
+namespace authentication {
+
+using std::map;
+using std::string;
+using std::vector;
+
+
+class JWTAuthenticatorProcess : public Process<JWTAuthenticatorProcess>
+{
+public:
+ JWTAuthenticatorProcess(const string& realm, const string& secret);
+
+ Future<AuthenticationResult> authenticate(const Request& request);
+
+private:
+ const string realm_;
+ const string secret_;
+};
+
+
+JWTAuthenticatorProcess::JWTAuthenticatorProcess(
+ const string& realm,
+ const string& secret)
+ : ProcessBase(ID::generate("__jwt_authenticator__")),
+ realm_(realm),
+ secret_(secret) {}
+
+
+Future<AuthenticationResult> JWTAuthenticatorProcess::authenticate(
+ const Request &request)
+{
+ AuthenticationResult result;
+
+ Option<string> header = request.headers.get("Authorization");
+
+ if (header.isNone()) {
+ // Requests without any authentication information shouldn't include
+ // error information (see RFC 6750, Section 3.1).
+ result.unauthorized = Unauthorized({"Bearer realm=\"" + realm_ + "\""});
+ return result;
+ }
+
+ const vector<string> token = strings::split(header.get(), " ");
+
+ if (token.size() != 2) {
+ result.unauthorized = Unauthorized({
+ "Bearer realm=\"" + realm_ + "\", "
+ "error=\"invalid_token\", "
+ "error_description=\"Malformed 'Authorization' header\""});
+ return result;
+ }
+
+ if (token[0] != "Bearer") {
+ result.unauthorized = Unauthorized({
+ "Bearer realm=\"" + realm_ + "\", "
+ "error=\"invalid_token\", "
+ "error_description=\"Scheme '" + token[0] + "' unsupported\""});
+ return result;
+ }
+
+ const Try<JWT, JWTError> jwt = JWT::parse(token[1], secret_);
+
+ if (jwt.isError()) {
+ switch (jwt.error().type) {
+ case JWTError::Type::INVALID_TOKEN:
+ result.unauthorized = Unauthorized({
+ "Bearer realm=\"" + realm_ + "\", "
+ "error=\"invalid_token\", "
+ "error_description=\"Invalid JWT: " + jwt.error().message + "\""});
+ return result;
+
+ case JWTError::Type::UNKNOWN:
+ return Failure(jwt.error());
+
+ UNREACHABLE();
+ }
+ }
+
+ Principal principal(Option<string>::none());
+
+ if (jwt->payload.values.empty()) {
+ result.unauthorized = Unauthorized({
+ "Bearer realm=\"" + realm_ + "\", "
+ "error=\"invalid_token\", "
+ "error_description=\"JWT claims missing\""});
+ return result;
+ }
+
+ foreachpair (
+ const string& key,
+ const JSON::Value& value,
+ jwt->payload.values) {
+ // Calling 'stringify' on a 'JSON::String' would result in a quoted
+ // string. Hence this case is treated differently.
+ if (value.is<JSON::String>()) {
+ principal.claims[key] = value.as<JSON::String>().value;
+ } else {
+ principal.claims[key] = stringify(value);
+ }
+ }
+
+ result.principal = principal;
+ return result;
+}
+
+
+JWTAuthenticator::JWTAuthenticator(const string& realm, const string& secret)
+ : process_(new JWTAuthenticatorProcess(realm, secret))
+{
+ spawn(process_.get());
+}
+
+
+JWTAuthenticator::~JWTAuthenticator()
+{
+ terminate(process_.get());
+ wait(process_.get());
+}
+
+
+Future<AuthenticationResult> JWTAuthenticator::authenticate(
+ const Request& request)
+{
+ return dispatch(
+ process_.get(),
+ &JWTAuthenticatorProcess::authenticate,
+ request);
+}
+
+
+string JWTAuthenticator::scheme() const
+{
+ return "Bearer";
+}
+
+} // namespace authentication {
+} // namespace http {
+} // namespace process {
http://git-wip-us.apache.org/repos/asf/mesos/blob/b4a16030/3rdparty/libprocess/src/tests/http_tests.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/src/tests/http_tests.cpp b/3rdparty/libprocess/src/tests/http_tests.cpp
index a0e23c2..09c7297 100644
--- a/3rdparty/libprocess/src/tests/http_tests.cpp
+++ b/3rdparty/libprocess/src/tests/http_tests.cpp
@@ -33,6 +33,9 @@
#include <process/http.hpp>
#include <process/id.hpp>
#include <process/io.hpp>
+#ifdef USE_SSL_SOCKET
+#include <process/jwt.hpp>
+#endif // USE_SSL_SOCKET
#include <process/owned.hpp>
#include <process/socket.hpp>
@@ -60,6 +63,11 @@ namespace unix = process::network::unix;
using authentication::Authenticator;
using authentication::AuthenticationResult;
using authentication::BasicAuthenticator;
+#ifdef USE_SSL_SOCKET
+using authentication::JWT;
+using authentication::JWTAuthenticator;
+using authentication::JWTError;
+#endif // USE_SSL_SOCKET
using authentication::Principal;
using process::Failure;
@@ -2011,6 +2019,85 @@ TEST_F(HttpAuthenticationTest, Basic)
}
+#ifdef USE_SSL_SOCKET
+// Tests the "JWT" authenticator.
+TEST_F(HttpAuthenticationTest, JWT)
+{
+ Http http;
+
+ Owned<Authenticator> authenticator(new JWTAuthenticator("realm", "secret"));
+
+ AWAIT_READY(setAuthenticator("realm", authenticator));
+
+ // No 'Authorization' header provided.
+ {
+ Future<http::Response> response = http::get(*http.process, "authenticated");
+
+ AWAIT_EXPECT_RESPONSE_STATUS_EQ(http::Unauthorized({}).status, response);
+ }
+
+ // Invalid 'Authorization' header provided.
+ {
+ http::Headers headers;
+ headers["Authorization"] = "Basic " + base64::encode("user:password");
+
+ Future<http::Response> response =
+ http::get(http.process->self(), "authenticated", None(), headers);
+
+ AWAIT_EXPECT_RESPONSE_STATUS_EQ(http::Unauthorized({}).status, response);
+ }
+
+ // Invalid token provided.
+ {
+ JSON::Object payload;
+ payload.values["sub"] = "user";
+
+ Try<JWT, JWTError> jwt = JWT::create(payload, "a different secret");
+
+ // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+ // once MESOS-7220 is resolved.
+ EXPECT_TRUE(jwt.isSome());
+
+ http::Headers headers;
+ headers["Authorization"] = "Bearer " + stringify(jwt.get());
+
+ Future<http::Response> response =
+ http::get(http.process->self(), "authenticated", None(), headers);
+
+ AWAIT_EXPECT_RESPONSE_STATUS_EQ(http::Unauthorized({}).status, response);
+ }
+
+ // Valid token provided.
+ {
+ Principal principal(Option<string>::none());
+ principal.claims["foo"] = "1234";
+ principal.claims["sub"] = "user";
+
+ EXPECT_CALL(*http.process, authenticated(_, Option<Principal>(principal)))
+ .WillOnce(Return(http::OK()));
+
+ JSON::Object payload;
+ payload.values["foo"] = 1234;
+ payload.values["sub"] = "user";
+
+ Try<JWT, JWTError> jwt = JWT::create(payload, "secret");
+
+ // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+ // once MESOS-7220 is resolved.
+ EXPECT_TRUE(jwt.isSome());
+
+ http::Headers headers;
+ headers["Authorization"] = "Bearer " + stringify(jwt.get());
+
+ Future<http::Response> response =
+ http::get(http.process->self(), "authenticated", None(), headers);
+
+ AWAIT_EXPECT_RESPONSE_STATUS_EQ(http::OK().status, response);
+ }
+}
+#endif // USE_SSL_SOCKET
+
+
class HttpServeTest : public TemporaryDirectoryTest {};