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 {};