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:27 UTC

[1/6] mesos git commit: Added a URL-safe base64 implementation.

Repository: mesos
Updated Branches:
  refs/heads/master ec319931c -> f37c51dfb


Added a URL-safe base64 implementation.

Base64 has many variants that use different alphabets for encoding.
"Base 64 Encoding with URL and Filename Safe Alphabet" is a variant
described in RFC 4648. This variant is used, among others, by JSON Web
Tokens (RFC 7519) to allow them to be part of the URL of a HTTP GET
request.

Review: https://reviews.apache.org/r/56665/


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/244f3e6a
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/244f3e6a
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/244f3e6a

Branch: refs/heads/master
Commit: 244f3e6afbdce0870bbd3744fe2e155ca4c597ad
Parents: ec31993
Author: Jan Schlicht <ja...@mesosphere.io>
Authored: Mon Mar 13 16:45:45 2017 +0100
Committer: Vinod Kone <vi...@gmail.com>
Committed: Mon Mar 13 16:45:45 2017 +0100

----------------------------------------------------------------------
 3rdparty/stout/include/stout/base64.hpp | 79 +++++++++++++++++++++++++---
 3rdparty/stout/tests/base64_tests.cpp   | 40 ++++++++++++++
 2 files changed, 112 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/244f3e6a/3rdparty/stout/include/stout/base64.hpp
----------------------------------------------------------------------
diff --git a/3rdparty/stout/include/stout/base64.hpp b/3rdparty/stout/include/stout/base64.hpp
index 2ac04c4..eabc9b0 100644
--- a/3rdparty/stout/include/stout/base64.hpp
+++ b/3rdparty/stout/include/stout/base64.hpp
@@ -23,17 +23,27 @@
 
 namespace base64 {
 
+namespace internal {
+
 // This slightly modified base64 implementation from
 // cplusplus.com answer by modoran can be found at:
 // http://www.cplusplus.com/forum/beginner/51572/
 
-static const std::string chars =
+constexpr char STANDARD_CHARS[] =
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
   "abcdefghijklmnopqrstuvwxyz"
   "0123456789+/";
 
+constexpr char URL_SAFE_CHARS[] =
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+  "abcdefghijklmnopqrstuvwxyz"
+  "0123456789-_";
 
-inline std::string encode(const std::string& s)
+
+inline std::string encode(
+    const std::string& s,
+    const std::string& chars,
+    bool padding)
 {
   std::string result;
   int i = 0;
@@ -68,8 +78,10 @@ inline std::string encode(const std::string& s)
     for (j = 0; j < i + 1; j++) {
       result += chars[array4[j]];
     }
-    while (i++ < 3) {
-      result += '=';
+    if (padding) {
+      while (i++ < 3) {
+        result += '=';
+      }
     }
   }
 
@@ -77,10 +89,10 @@ inline std::string encode(const std::string& s)
 }
 
 
-inline Try<std::string> decode(const std::string& s)
+inline Try<std::string> decode(const std::string& s, const std::string& chars)
 {
-  auto isBase64 = [](unsigned char c) -> bool {
-    return (isalnum(c) || (c == '+') || (c == '/'));
+  auto isBase64 = [&chars](unsigned char c) -> bool {
+    return (isalnum(c) || (c == chars[62]) || (c == chars[63]));
   };
 
   size_t i = 0;
@@ -135,6 +147,59 @@ inline Try<std::string> decode(const std::string& s)
   return result;
 }
 
+} // namespace internal {
+
+
+/**
+ * Encode a string to Base64 with the standard Base64 alphabet.
+ * @see <a href="https://tools.ietf.org/html/rfc4648#section-4">RFC4648</a>
+ *
+ * @param s The string to encode.
+ */
+inline std::string encode(const std::string& s)
+{
+  return internal::encode(s, internal::STANDARD_CHARS, true);
+}
+
+
+/**
+ * Decode a string that is Base64-encoded with the standard Base64
+ * alphabet.
+ * @see <a href="https://tools.ietf.org/html/rfc4648#section-4">RFC4648</a>
+ *
+ * @param s The string to decode.
+ */
+inline Try<std::string> decode(const std::string& s)
+{
+  return internal::decode(s, internal::STANDARD_CHARS);
+}
+
+
+/**
+ * Encode a string to Base64 with a URL and filename safe alphabet.
+ * @see <a href="https://tools.ietf.org/html/rfc4648#section-5">RFC4648</a>
+ *
+ * @param s The string to encode.
+ * @param padding True if padding characters ('=') should be added.
+ */
+inline std::string encode_url_safe(const std::string& s, bool padding = true)
+{
+  return internal::encode(s, internal::URL_SAFE_CHARS, padding);
+}
+
+
+/**
+ * Decode a string that is Base64-encoded with a URL and filename safe
+ * alphabet.
+ * @see <a href="https://tools.ietf.org/html/rfc4648#section-5">RFC4648</a>
+ *
+ * @param s The string to decode.
+ */
+inline Try<std::string> decode_url_safe(const std::string& s)
+{
+  return internal::decode(s, internal::URL_SAFE_CHARS);
+}
+
 } // namespace base64 {
 
 #endif // __STOUT_BASE64_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/244f3e6a/3rdparty/stout/tests/base64_tests.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/stout/tests/base64_tests.cpp b/3rdparty/stout/tests/base64_tests.cpp
index 32e5168..0473221 100644
--- a/3rdparty/stout/tests/base64_tests.cpp
+++ b/3rdparty/stout/tests/base64_tests.cpp
@@ -43,3 +43,43 @@ TEST(Base64Test, Decode)
   //  EXPECT_ERROR(base64::decode("ab=,"));
   //  EXPECT_ERROR(base64::decode("ab==="));
 }
+
+
+TEST(Base64Test, EncodeURLSafe)
+{
+  EXPECT_EQ(
+      "dXNlcjpwYXNzd29yZH5-fg",
+      base64::encode_url_safe("user:password~~~", false));
+
+  EXPECT_EQ(
+      "dXNlcjpwYXNzd29yZH5-fg==",
+      base64::encode_url_safe("user:password~~~", true));
+
+  EXPECT_EQ(
+      "fn5-w7_Dv8O_w78",
+      base64::encode_url_safe("~~~\u00ff\u00ff\u00ff\u00ff", false));
+
+  EXPECT_EQ(
+      "fn5-w7_Dv8O_w78=",
+      base64::encode_url_safe("~~~\u00ff\u00ff\u00ff\u00ff", true));
+}
+
+
+TEST(Base64Test, DecodeURLSafe)
+{
+  EXPECT_SOME_EQ(
+      "user:password~~~",
+      base64::decode_url_safe("dXNlcjpwYXNzd29yZH5-fg"));
+
+  EXPECT_SOME_EQ(
+      "user:password~~~",
+      base64::decode_url_safe("dXNlcjpwYXNzd29yZH5-fg=="));
+
+  EXPECT_SOME_EQ(
+      "~~~\u00ff\u00ff\u00ff\u00ff",
+      base64::decode_url_safe("fn5-w7_Dv8O_w78"));
+
+  EXPECT_SOME_EQ(
+      "~~~\u00ff\u00ff\u00ff\u00ff",
+      base64::decode_url_safe("fn5-w7_Dv8O_w78="));
+}


[5/6] mesos git commit: Added the SecretGenerator module interface.

Posted by vi...@apache.org.
Added the SecretGenerator module interface.

This interface will be used by agents to create credentials for the
default executor.

Review: https://reviews.apache.org/r/56757/


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/686733dd
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/686733dd
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/686733dd

Branch: refs/heads/master
Commit: 686733dd7b97b2d0623eabbb49242d4900baa523
Parents: b4a1603
Author: Jan Schlicht <ja...@mesosphere.io>
Authored: Mon Mar 13 16:46:09 2017 +0100
Committer: Vinod Kone <vi...@gmail.com>
Committed: Mon Mar 13 16:46:09 2017 +0100

----------------------------------------------------------------------
 .../mesos/authentication/secret_generator.hpp   | 49 +++++++++++++++
 include/mesos/module/secret_generator.hpp       | 64 ++++++++++++++++++++
 src/Makefile.am                                 |  4 +-
 3 files changed, 116 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/686733dd/include/mesos/authentication/secret_generator.hpp
----------------------------------------------------------------------
diff --git a/include/mesos/authentication/secret_generator.hpp b/include/mesos/authentication/secret_generator.hpp
new file mode 100644
index 0000000..f2fb0e7
--- /dev/null
+++ b/include/mesos/authentication/secret_generator.hpp
@@ -0,0 +1,49 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+#ifndef __MESOS_AUTHENTICATION_SECRET_GENERATOR_HPP__
+#define __MESOS_AUTHENTICATION_SECRET_GENERATOR_HPP__
+
+#include <mesos/mesos.hpp>
+
+#include <process/future.hpp>
+#include <process/http.hpp>
+
+namespace mesos {
+namespace http {
+namespace authentication {
+
+/**
+ * The SecretGenerator interface represents a mechanism to create a secret
+ * from a principal. The secret is meant to contain a token (for example,
+ * an HTTP 'Authorization' header) that can be used to authenticate. If
+ * used that way, an authenticator will yield the same principal that
+ * was passed to the `generate` method of this module.
+ */
+class SecretGenerator
+{
+public:
+  virtual ~SecretGenerator() {}
+
+  virtual process::Future<Secret> generate(
+      const process::http::authentication::Principal& principal) = 0;
+};
+
+} // namespace authentication {
+} // namespace http {
+} // namespace mesos {
+
+#endif // __MESOS_AUTHENTICATION_SECRET_GENERATOR_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/686733dd/include/mesos/module/secret_generator.hpp
----------------------------------------------------------------------
diff --git a/include/mesos/module/secret_generator.hpp b/include/mesos/module/secret_generator.hpp
new file mode 100644
index 0000000..c8b7d8c
--- /dev/null
+++ b/include/mesos/module/secret_generator.hpp
@@ -0,0 +1,64 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+#ifndef __MESOS_MODULE_SECRET_GENERATOR_HPP__
+#define __MESOS_MODULE_SECRET_GENERATOR_HPP__
+
+#include <mesos/mesos.hpp>
+#include <mesos/module.hpp>
+
+#include <mesos/authentication/secret_generator.hpp>
+
+namespace mesos {
+namespace modules {
+
+template <>
+inline const char* kind<mesos::http::authentication::SecretGenerator>()
+{
+  return "SecretGenerator";
+}
+
+
+template <>
+struct Module<mesos::http::authentication::SecretGenerator> : ModuleBase
+{
+  Module(
+      const char* _moduleApiVersion,
+      const char* _mesosVersion,
+      const char* _authorName,
+      const char* _authorEmail,
+      const char* _description,
+      bool (*_compatible)(),
+      mesos::http::authentication::SecretGenerator* (*_create)(
+          const Parameters& parameters))
+    : ModuleBase(
+        _moduleApiVersion,
+        _mesosVersion,
+        mesos::modules::kind<mesos::http::authentication::SecretGenerator>(),
+        _authorName,
+        _authorEmail,
+        _description,
+        _compatible),
+      create(_create) {}
+
+  mesos::http::authentication::SecretGenerator* (*create)(
+      const Parameters& parameters);
+};
+
+} // namespace modules {
+} // namespace mesos {
+
+#endif // __MESOS_MODULE_SECRET_GENERATOR_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/686733dd/src/Makefile.am
----------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index bb917c5..9be578b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -560,6 +560,7 @@ authentication_HEADERS =						\
   $(top_srcdir)/include/mesos/authentication/authentication.hpp		\
   $(top_srcdir)/include/mesos/authentication/authentication.proto	\
   $(top_srcdir)/include/mesos/authentication/authenticator.hpp		\
+  $(top_srcdir)/include/mesos/authentication/secret_generator.hpp	\
   $(top_srcdir)/include/mesos/authentication/http/basic_authenticator_factory.hpp
 
 nodist_authentication_HEADERS =						\
@@ -653,7 +654,8 @@ module_HEADERS =							\
   $(top_srcdir)/include/mesos/module/qos_controller.hpp			\
   $(top_srcdir)/include/mesos/module/resource_estimator.hpp		\
   $(top_srcdir)/include/mesos/module/contender.hpp			\
-  $(top_srcdir)/include/mesos/module/detector.hpp
+  $(top_srcdir)/include/mesos/module/detector.hpp			\
+  $(top_srcdir)/include/mesos/module/secret_generator.hpp
 
 nodist_module_HEADERS =							\
   ../include/mesos/module/hook.pb.h					\


[2/6] mesos git commit: Added a HMAC SHA256 generator.

Posted by vi...@apache.org.
Added a HMAC SHA256 generator.

HMAC SHA256 can be used to create or verify message signatures.

Review: https://reviews.apache.org/r/56666/


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/f1d0a1c2
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/f1d0a1c2
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/f1d0a1c2

Branch: refs/heads/master
Commit: f1d0a1c2beeb20a26f1ccab1ceb2fab1546249bf
Parents: 244f3e6
Author: Jan Schlicht <ja...@mesosphere.io>
Authored: Mon Mar 13 16:45:54 2017 +0100
Committer: Vinod Kone <vi...@gmail.com>
Committed: Mon Mar 13 16:45:54 2017 +0100

----------------------------------------------------------------------
 .../include/process/ssl/utilities.hpp           | 14 +++++++++
 3rdparty/libprocess/src/ssl/utilities.cpp       | 30 ++++++++++++++++++++
 2 files changed, 44 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/f1d0a1c2/3rdparty/libprocess/include/process/ssl/utilities.hpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/include/process/ssl/utilities.hpp b/3rdparty/libprocess/include/process/ssl/utilities.hpp
index c2f64a9..797353c 100644
--- a/3rdparty/libprocess/include/process/ssl/utilities.hpp
+++ b/3rdparty/libprocess/include/process/ssl/utilities.hpp
@@ -110,6 +110,20 @@ Try<Nothing> write_key_file(EVP_PKEY* private_key, const Path& path);
  */
 Try<Nothing> write_certificate_file(X509* x509, const Path& path);
 
+
+/**
+ * Generates a keyed-hash message authentication code (HMAC) with SHA256.
+ * @see <a href="https://www.openssl.org/docs/man1.1.0/crypto/HMAC.html">HMAC</a> // NOLINT
+ *
+ * @param message The message to be authenticated.
+ * @param key The secret key.
+ *
+ * @return The HMAC if successful otherwise an Error.
+ */
+Try<std::string> generate_hmac_sha256(
+    const std::string& message,
+    const std::string& key);
+
 } // namespace openssl {
 } // namespace network {
 } // namespace process {

http://git-wip-us.apache.org/repos/asf/mesos/blob/f1d0a1c2/3rdparty/libprocess/src/ssl/utilities.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/src/ssl/utilities.cpp b/3rdparty/libprocess/src/ssl/utilities.cpp
index 8aec613..d752acb 100644
--- a/3rdparty/libprocess/src/ssl/utilities.cpp
+++ b/3rdparty/libprocess/src/ssl/utilities.cpp
@@ -13,10 +13,14 @@
 #include <process/ssl/utilities.hpp>
 
 #include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
 #include <openssl/rsa.h>
 #include <openssl/x509.h>
 #include <openssl/x509v3.h>
 
+#include <string>
+
 #include <stout/check.hpp>
 #include <stout/net.hpp>
 #include <stout/stringify.hpp>
@@ -336,6 +340,32 @@ Try<Nothing> write_certificate_file(X509* x509, const Path& path)
   return Nothing();
 }
 
+
+Try<std::string> generate_hmac_sha256(
+  const std::string& message,
+  const std::string& key)
+{
+  unsigned int md_len = 0;
+
+  unsigned char* rc = HMAC(
+      EVP_sha256(),
+      key.data(),
+      key.size(),
+      reinterpret_cast<const unsigned char*>(message.data()),
+      message.size(),
+      nullptr,
+      &md_len);
+
+  if (rc == nullptr) {
+    const char* reason = ERR_reason_error_string(ERR_get_error());
+
+    return Error(
+        "HMAC failed" + (reason == nullptr ? "" : ": " + std::string(reason)));
+  }
+
+  return std::string(reinterpret_cast<char*>(rc), md_len);
+}
+
 } // namespace openssl {
 } // namespace network {
 } // namespace process {


[6/6] mesos git commit: Implemented a JWT secret generator.

Posted by vi...@apache.org.
Implemented a JWT secret generator.

This can be used to create a 'Secret' from a 'Principal'.
The resulting secret will provide a JWT.

Review: https://reviews.apache.org/r/56754/


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/f37c51df
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/f37c51df
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/f37c51df

Branch: refs/heads/master
Commit: f37c51dfb86cb4065c464c37eba303ff64450666
Parents: 686733d
Author: Jan Schlicht <ja...@mesosphere.io>
Authored: Mon Mar 13 16:46:14 2017 +0100
Committer: Vinod Kone <vi...@gmail.com>
Committed: Mon Mar 13 16:46:14 2017 +0100

----------------------------------------------------------------------
 src/Makefile.am                                 | 11 +++
 .../executor/jwt_secret_generator.cpp           | 75 ++++++++++++++++++
 .../executor/jwt_secret_generator.hpp           | 55 +++++++++++++
 src/tests/secret_generator_tests.cpp            | 83 ++++++++++++++++++++
 4 files changed, 224 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/f37c51df/src/Makefile.am
----------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index 9be578b..2eea11a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1281,6 +1281,12 @@ else
 EXTRA_DIST += $(MESOS_NETWORK_ISOLATOR_FILES)
 endif
 
+if ENABLE_SSL
+libmesos_no_3rdparty_la_SOURCES +=					\
+  authentication/executor/jwt_secret_generator.cpp				\
+  authentication/executor/jwt_secret_generator.hpp
+endif
+
 libmesos_no_3rdparty_la_CPPFLAGS = $(MESOS_CPPFLAGS)
 
 libmesos_no_3rdparty_la_LIBADD = # Initialized to enable using +=.
@@ -2381,6 +2387,11 @@ mesos_tests_SOURCES +=						\
   tests/containerizer/routing_tests.cpp
 endif
 
+if ENABLE_SSL
+mesos_tests_SOURCES +=						\
+  tests/secret_generator_tests.cpp
+endif
+
 if HAS_JAVA
 mesos_tests_SOURCES +=						\
   tests/group_tests.cpp						\

http://git-wip-us.apache.org/repos/asf/mesos/blob/f37c51df/src/authentication/executor/jwt_secret_generator.cpp
----------------------------------------------------------------------
diff --git a/src/authentication/executor/jwt_secret_generator.cpp b/src/authentication/executor/jwt_secret_generator.cpp
new file mode 100644
index 0000000..6aed6bd
--- /dev/null
+++ b/src/authentication/executor/jwt_secret_generator.cpp
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 "jwt_secret_generator.hpp"
+
+#include <process/jwt.hpp>
+
+#include <stout/json.hpp>
+#include <stout/stringify.hpp>
+
+namespace mesos {
+namespace http {
+namespace authentication {
+
+using process::Failure;
+using process::Future;
+
+using process::http::authentication::JWT;
+using process::http::authentication::JWTError;
+using process::http::authentication::Principal;
+
+using std::string;
+
+
+JWTSecretGenerator::JWTSecretGenerator(const std::string& secret)
+  : secret_(secret) {}
+
+
+JWTSecretGenerator::~JWTSecretGenerator() {}
+
+
+Future<Secret> JWTSecretGenerator::generate(const Principal& principal)
+{
+  if (principal.value.isSome()) {
+    return Failure("Principal has a value, but only claims are supported");
+  }
+
+  JSON::Object payload;
+
+  foreachpair (const string& key, const string& value, principal.claims) {
+    payload.values[key] = value;
+  }
+
+  Try<JWT, JWTError> jwt = JWT::create(payload, secret_);
+
+  if (jwt.isError()) {
+    return Failure("Error creating JWT: " + jwt.error().message);
+  }
+
+  Secret::Value value;
+  value.set_data(stringify(jwt.get()));
+
+  Secret result;
+  result.set_type(Secret::VALUE);
+  result.mutable_value()->CopyFrom(value);
+
+  return result;
+}
+
+} // namespace authentication {
+} // namespace http {
+} // namespace mesos {

http://git-wip-us.apache.org/repos/asf/mesos/blob/f37c51df/src/authentication/executor/jwt_secret_generator.hpp
----------------------------------------------------------------------
diff --git a/src/authentication/executor/jwt_secret_generator.hpp b/src/authentication/executor/jwt_secret_generator.hpp
new file mode 100644
index 0000000..a945358
--- /dev/null
+++ b/src/authentication/executor/jwt_secret_generator.hpp
@@ -0,0 +1,55 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+#ifndef __MESOS_AUTHENTICATION_EXECUTOR_JWT_SECRET_GENERATOR_HPP__
+#define __MESOS_AUTHENTICATION_EXECUTOR_JWT_SECRET_GENERATOR_HPP__
+
+#include <string>
+
+#include <mesos/authentication/secret_generator.hpp>
+
+#include <process/authenticator.hpp>
+#include <process/future.hpp>
+
+namespace mesos {
+namespace http {
+namespace authentication {
+
+/**
+ * Creates a VALUE-type secret containing a JWT. When the secret is
+ * presented to the 'JWTAuthenticator' module, the authenticator will
+ * return the principal which is provided here as an argument.
+ */
+class JWTSecretGenerator : public SecretGenerator
+{
+public:
+  JWTSecretGenerator(const std::string& secret);
+
+  ~JWTSecretGenerator() override;
+
+  process::Future<Secret> generate(
+      const process::http::authentication::Principal& principal)
+    override;
+
+private:
+  std::string secret_;
+};
+
+} // namespace authentication {
+} // namespace http {
+} // namespace mesos {
+
+#endif // __MESOS_AUTHENTICATION_EXECUTOR_JWT_SECRET_GENERATOR_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/f37c51df/src/tests/secret_generator_tests.cpp
----------------------------------------------------------------------
diff --git a/src/tests/secret_generator_tests.cpp b/src/tests/secret_generator_tests.cpp
new file mode 100644
index 0000000..81fd54d
--- /dev/null
+++ b/src/tests/secret_generator_tests.cpp
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 <gtest/gtest.h>
+
+#include <map>
+#include <string>
+
+#include <mesos/mesos.hpp>
+
+#include <process/authenticator.hpp>
+#include <process/future.hpp>
+#include <process/gtest.hpp>
+#include <process/jwt.hpp>
+
+#include <stout/gtest.hpp>
+#include <stout/json.hpp>
+
+#include "authentication/executor/jwt_secret_generator.hpp"
+
+namespace mesos {
+namespace internal {
+namespace tests {
+
+using mesos::http::authentication::JWTSecretGenerator;
+
+using process::Future;
+
+using process::http::authentication::JWT;
+using process::http::authentication::JWTError;
+using process::http::authentication::Principal;
+
+using std::string;
+
+
+TEST(JWTSecretGeneratorTest, Generate)
+{
+  const string secret = "secret";
+
+  JWTSecretGenerator generator(secret);
+
+  Principal principal(Option<string>::none());
+  principal.claims["sub"] = "user";
+  principal.claims["foo"] = "bar";
+
+  const Future<Secret> token = generator.generate(principal);
+
+  AWAIT_READY(token);
+
+  EXPECT_TRUE(token->has_value());
+  EXPECT_TRUE(token->value().has_data());
+
+  const Try<JWT, JWTError> jwt = JWT::parse(token->value().data(), secret);
+
+  // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+  // once MESOS-7220 is resolved.
+  EXPECT_TRUE(jwt.isSome());
+
+  Result<JSON::String> sub = jwt->payload.at<JSON::String>("sub");
+
+  EXPECT_SOME_EQ("user", sub);
+
+  Result<JSON::String> foo = jwt->payload.at<JSON::String>("foo");
+
+  EXPECT_SOME_EQ("bar", foo);
+}
+
+} // namespace tests {
+} // namespace internal {
+} // namespace mesos {


[3/6] mesos git commit: Added support for JSON Web Tokens.

Posted by vi...@apache.org.
Added support for JSON Web Tokens.

JSON Web Tokens can be used to create claim-based access tokens and is
typically used for HTTP authentication.
This implementation is intended for internal use, e.g. Mesos is supposed
to only parse tokens that it also created. It doesn't fully comply with
RFC 7519. Currently the only supported cryptographic algorithm is HMAC
with SHA-256.

Review: https://reviews.apache.org/r/56667/


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/51140afd
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/51140afd
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/51140afd

Branch: refs/heads/master
Commit: 51140afd1fe28958fee56b5f671af062086226fa
Parents: f1d0a1c
Author: Jan Schlicht <ja...@mesosphere.io>
Authored: Mon Mar 13 16:45:59 2017 +0100
Committer: Vinod Kone <vi...@gmail.com>
Committed: Mon Mar 13 16:45:59 2017 +0100

----------------------------------------------------------------------
 3rdparty/libprocess/Makefile.am             |   2 +
 3rdparty/libprocess/include/process/jwt.hpp | 134 +++++++++
 3rdparty/libprocess/src/jwt.cpp             | 339 +++++++++++++++++++++++
 3rdparty/libprocess/src/tests/jwt_tests.cpp | 292 +++++++++++++++++++
 4 files changed, 767 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/51140afd/3rdparty/libprocess/Makefile.am
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/Makefile.am b/3rdparty/libprocess/Makefile.am
index 7538618..18bcabb 100644
--- a/3rdparty/libprocess/Makefile.am
+++ b/3rdparty/libprocess/Makefile.am
@@ -181,6 +181,7 @@ libprocess_la_SOURCES =		\
 
 if ENABLE_SSL
 libprocess_la_SOURCES +=	\
+    src/jwt.cpp			\
     src/libevent_ssl_socket.cpp	\
     src/libevent_ssl_socket.hpp	\
     src/openssl.cpp		\
@@ -277,6 +278,7 @@ ssl_client_CPPFLAGS = $(libprocess_tests_CPPFLAGS)
 ssl_client_LDADD = $(libprocess_tests_LDADD)
 
 libprocess_tests_SOURCES +=		\
+  src/tests/jwt_tests.cpp		\
   src/tests/ssl_tests.cpp
 endif
 

http://git-wip-us.apache.org/repos/asf/mesos/blob/51140afd/3rdparty/libprocess/include/process/jwt.hpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/include/process/jwt.hpp b/3rdparty/libprocess/include/process/jwt.hpp
new file mode 100644
index 0000000..768cbf6
--- /dev/null
+++ b/3rdparty/libprocess/include/process/jwt.hpp
@@ -0,0 +1,134 @@
+// 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
+
+#ifndef __PROCESS_JWT_HPP__
+#define __PROCESS_JWT_HPP__
+
+#include <ostream>
+#include <string>
+
+#include <stout/json.hpp>
+#include <stout/option.hpp>
+#include <stout/try.hpp>
+
+namespace process {
+namespace http {
+namespace authentication {
+
+// Represents the various errors that can be returned when parsing or
+// creating JSON Web Tokens. This can be useful to create proper
+// responses to HTTP requests that included a token.
+class JWTError : public Error {
+public:
+  enum class Type {
+    INVALID_TOKEN, // Invalid token.
+    UNKNOWN        // Internal error/all other errors.
+  };
+
+  JWTError(const std::string& message, Type _type)
+    : Error(message), type(_type) {};
+
+  const Type type;
+};
+
+
+/**
+ * A JSON Web Token (JWT) implementation.
+ * @see <a href="https://tools.ietf.org/html/rfc7519">RFC 7519</a>
+ *
+ * This implementation supports the 'none' and 'HS256' algorithms.
+ * Header parameters other than 'alg' and 'typ' aren't parsed. To comply
+ * with RFC 7515, headers with 'crit' parameter are invalid.
+ * Currently, only the 'exp' standard claim is validated. Applications
+ * that need to validate other claims need to do this in their
+ * validation logic.
+ */
+class JWT
+{
+public:
+  enum class Alg
+  {
+    None,
+    HS256
+  };
+
+  struct Header
+  {
+    Alg alg;
+    Option<std::string> typ;
+  };
+
+  /**
+   * Parse an unsecured JWT.
+   *
+   * @param token The JWT to parse.
+   *
+   * @return The JWT representation if successful otherwise an Error.
+   */
+  static Try<JWT, JWTError> parse(const std::string& token);
+
+  /**
+   * Parse a JWT and validate its HS256 signature.
+   *
+   * @param token The JWT to parse.
+   * @param secret The secret to validate the signature with.
+   *
+   * @return The validated JWT representation if successful otherwise an
+   *     Error.
+   */
+  static Try<JWT, JWTError> parse(
+      const std::string& token,
+      const std::string& secret);
+
+  /**
+   * Create an unsecured JWT.
+   *
+   * @param payload The payload of the JWT.
+   *
+   * @return The unsecured JWT representation if successful otherwise an
+   *     Error.
+   */
+  static Try<JWT, JWTError> create(const JSON::Object& payload);
+
+  /**
+   * Create a JWT with a HS256 signature.
+   *
+   * When creating a payload keep in mind that of the standard claims
+   * currently only 'exp' is validated during parsing.
+   *
+   * @param payload The payload of the JWT
+   * @param secret The secret to sign the JWT with.
+   *
+   * @return The signed JWT representation if successful otherwise an
+   *     Error.
+   */
+  static Try<JWT, JWTError> create(
+      const JSON::Object& payload,
+      const std::string& secret);
+
+  const Header header;
+  const JSON::Object payload;
+  const Option<std::string> signature;
+
+private:
+  JWT(const Header& header,
+      const JSON::Object& payload,
+      const Option<std::string>& signature);
+};
+
+std::ostream& operator<<(std::ostream& stream, const JWT& jwt);
+
+} // namespace authentication {
+} // namespace http {
+} // namespace process {
+
+#endif // __PROCESS_JWT_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/51140afd/3rdparty/libprocess/src/jwt.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/src/jwt.cpp b/3rdparty/libprocess/src/jwt.cpp
new file mode 100644
index 0000000..921031e
--- /dev/null
+++ b/3rdparty/libprocess/src/jwt.cpp
@@ -0,0 +1,339 @@
+// 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/jwt.hpp>
+
+#include <vector>
+
+#include <process/clock.hpp>
+
+#include <process/ssl/utilities.hpp>
+
+#include <stout/base64.hpp>
+#include <stout/strings.hpp>
+
+namespace process {
+namespace http {
+namespace authentication {
+
+using process::Clock;
+
+using process::network::openssl::generate_hmac_sha256;
+
+using std::ostream;
+using std::string;
+using std::vector;
+
+
+namespace {
+
+Try<JSON::Object> decode(const string& component)
+{
+  const Try<string> decoded = base64::decode_url_safe(component);
+
+  if (decoded.isError()) {
+    return Error("Failed to base64url-decode: " + decoded.error());
+  }
+
+  const Try<JSON::Object> json = JSON::parse<JSON::Object>(decoded.get());
+
+  if (json.isError()) {
+    return Error("Failed to parse into JSON: " + json.error());
+  }
+
+  return json;
+}
+
+
+Try<JWT::Header> parse_header(const string& component)
+{
+  Try<JSON::Object> header = decode(component);
+
+  if (header.isError()) {
+    return Error("Failed to decode token header: " + header.error());
+  }
+
+  // Validate JOSE header.
+
+  Option<string> typ = None();
+
+  const Result<JSON::Value> typ_json = header->find<JSON::Value>("typ");
+
+  if (typ_json.isSome()) {
+    if (!typ_json->is<JSON::String>()) {
+      return Error("Token 'typ' is not a string");
+    }
+
+    typ = typ_json->as<JSON::String>().value;
+  }
+
+  const Result<JSON::Value> alg_json = header->find<JSON::Value>("alg");
+
+  if (alg_json.isNone()) {
+    return Error("Failed to locate 'alg' in token JSON header");
+  }
+
+  if (alg_json.isError()) {
+    return Error(
+        "Error when extracting 'alg' field from token JSON header: " +
+        alg_json.error());
+  }
+
+  if (!alg_json->is<JSON::String>()) {
+    return Error("Token 'alg' field is not a string");
+  }
+
+  const string alg_value = alg_json->as<JSON::String>().value;
+
+  JWT::Alg alg;
+
+  if (alg_value == "none") {
+    alg = JWT::Alg::None;
+  } else if (alg_value == "HS256") {
+    alg = JWT::Alg::HS256;
+  } else {
+    return Error("Unsupported token algorithm: " + alg_value);
+  }
+
+  const Result<JSON::Value> crit_json = header->find<JSON::Value>("crit");
+
+  // The 'crit' header parameter indicates extensions that must be understood.
+  // As we're not supporting any extensions, the JWT header is deemed to be
+  // invalid upon the presence of this parameter.
+  if (crit_json.isSome()) {
+    return Error("Token 'crit' field is unsupported");
+  }
+
+  return JWT::Header{alg, typ};
+}
+
+
+Try<JSON::Object> parse_payload(const string& component)
+{
+  Try<JSON::Object> payload = decode(component);
+
+  if (payload.isError()) {
+    return Error("Failed to decode token payload: " + payload.error());
+  }
+
+  // Validate standard claims.
+
+  const Result<JSON::Value> exp_json = payload->find<JSON::Value>("exp");
+
+  if (exp_json.isError()) {
+    return Error(
+        "Error when extracting 'exp' field from token JSON payload: " +
+        exp_json.error());
+  }
+
+  if (exp_json.isSome()) {
+    if (!exp_json->is<JSON::Number>()) {
+      return Error("JSON payload 'exp' field is not a number");
+    }
+
+    const int64_t exp = exp_json->as<JSON::Number>().as<int64_t>();
+    const int64_t now = Clock::now().secs();
+
+    if (exp < now) {
+      return Error(
+          "Token has expired: exp(" +
+          stringify(exp) + ") < now(" + stringify(now) + ")");
+    }
+  }
+
+  // TODO(nfnt): Validate other standard claims.
+  return payload;
+}
+
+} // namespace {
+
+
+Try<JWT, JWTError> JWT::parse(const std::string& token)
+{
+  const vector<string> components = strings::split(token, ".");
+
+  if (components.size() != 3) {
+    return JWTError(
+        "Expected 3 components in token, got " + stringify(components.size()),
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  Try<JWT::Header> header = parse_header(components[0]);
+
+  if (header.isError()) {
+    return JWTError(header.error(), JWTError::Type::INVALID_TOKEN);
+  }
+
+  if (header->alg != JWT::Alg::None) {
+    return JWTError(
+        "Token 'alg' value \"" + stringify(header->alg) +
+        "\" does not match, expected \"none\"",
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  Try<JSON::Object> payload = parse_payload(components[1]);
+
+  if (payload.isError()) {
+    return JWTError(payload.error(), JWTError::Type::INVALID_TOKEN);
+  }
+
+  if (!components[2].empty()) {
+    return JWTError(
+        "Unsecured JWT contains a signature",
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  return JWT(header.get(), payload.get(), None());
+}
+
+
+Try<JWT, JWTError> JWT::parse(const string& token, const string& secret)
+{
+  const vector<string> components = strings::split(token, ".");
+
+  if (components.size() != 3) {
+    return JWTError(
+        "Expected 3 components in token, got " + stringify(components.size()),
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  Try<JWT::Header> header = parse_header(components[0]);
+
+  if (header.isError()) {
+    return JWTError(header.error(), JWTError::Type::INVALID_TOKEN);
+  }
+
+  if (header->alg != JWT::Alg::HS256) {
+    return JWTError(
+        "Token 'alg' value \"" + stringify(header->alg) +
+        "\" does not match, expected \"HS256\"",
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  Try<JSON::Object> payload = parse_payload(components[1]);
+
+  if (payload.isError()) {
+    return JWTError(payload.error(), JWTError::Type::INVALID_TOKEN);
+  }
+
+  const Try<string> signature = base64::decode_url_safe(components[2]);
+
+  if (signature.isError()) {
+    return JWTError(
+        "Failed to base64url-decode token signature: " + signature.error(),
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  // Validate HMAC SHA256 signature.
+
+  Try<string> hmac = generate_hmac_sha256(
+      components[0] + "." + components[1],
+      secret);
+
+  if (hmac.isError()) {
+    return JWTError(
+        "Failed to generate HMAC signature: " + hmac.error(),
+        JWTError::Type::UNKNOWN);
+  }
+
+  const bool valid = hmac.get() == signature.get();
+
+  if (!valid) {
+    return JWTError(
+        "Token signature does not match",
+        JWTError::Type::INVALID_TOKEN);
+  }
+
+  return JWT(header.get(), payload.get(), signature.get());
+}
+
+
+Try<JWT, JWTError> JWT::create(const JSON::Object& payload)
+{
+  const Header header{Alg::None, "JWT"};
+
+  return JWT(header, payload, None());
+}
+
+
+Try<JWT, JWTError> JWT::create(
+    const JSON::Object& payload,
+    const string& secret)
+{
+  const Header header{Alg::HS256, "JWT"};
+
+  const Try<string> hmac = generate_hmac_sha256(
+      base64::encode_url_safe(stringify(header), false) + "." +
+        base64::encode_url_safe(stringify(payload), false),
+      secret);
+
+  if (hmac.isError()) {
+    return JWTError(
+        "Failed to generate HMAC signature: " + hmac.error(),
+        JWTError::Type::UNKNOWN);
+  }
+
+  return JWT(header, payload, base64::encode_url_safe(hmac.get(), false));
+}
+
+
+JWT::JWT(
+    const Header& _header,
+    const JSON::Object& _payload,
+    const Option<string>& _signature)
+  : header(_header), payload(_payload), signature(_signature) {}
+
+
+ostream& operator<<(ostream& stream, const JWT::Alg& alg)
+{
+  switch (alg) {
+    case JWT::Alg::None:
+      stream << "none";
+      break;
+    case JWT::Alg::HS256:
+      stream << "HS256";
+      break;
+  }
+
+  return stream;
+}
+
+
+ostream& operator<<(ostream& stream, const JWT::Header& header)
+{
+  JSON::Object json;
+
+  json.values["alg"] = stringify(header.alg);
+  if (header.typ.isSome()) {
+    json.values["typ"] = header.typ.get();
+  }
+
+  stream << stringify(json);
+  return stream;
+}
+
+
+ostream& operator<<(ostream& stream, const JWT& jwt)
+{
+  stream << base64::encode_url_safe(stringify(jwt.header), false) + "."
+         << base64::encode_url_safe(stringify(jwt.payload), false) + ".";
+
+  if (jwt.signature.isSome()) {
+    stream << jwt.signature.get();
+  }
+
+  return stream;
+}
+
+} // namespace authentication {
+} // namespace http {
+} // namespace process {

http://git-wip-us.apache.org/repos/asf/mesos/blob/51140afd/3rdparty/libprocess/src/tests/jwt_tests.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/libprocess/src/tests/jwt_tests.cpp b/3rdparty/libprocess/src/tests/jwt_tests.cpp
new file mode 100644
index 0000000..eb36a9a
--- /dev/null
+++ b/3rdparty/libprocess/src/tests/jwt_tests.cpp
@@ -0,0 +1,292 @@
+// 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 <gtest/gtest.h>
+
+#include <string>
+
+#include <process/jwt.hpp>
+
+#include <process/ssl/utilities.hpp>
+
+#include <stout/base64.hpp>
+#include <stout/gtest.hpp>
+#include <stout/stringify.hpp>
+#include <stout/strings.hpp>
+
+using process::http::authentication::JWT;
+using process::http::authentication::JWTError;
+
+using process::network::openssl::generate_hmac_sha256;
+
+using std::string;
+
+
+TEST(JWTTest, Parse)
+{
+  const string secret = "secret";
+
+  auto create_token = [secret](string header, string payload) {
+    header = base64::encode_url_safe(header, false);
+    payload = base64::encode_url_safe(payload, false);
+
+    const string mac =
+      generate_hmac_sha256(strings::join(".", header, payload), secret).get();
+
+    const string signature = base64::encode_url_safe(mac, false);
+
+    return strings::join(".", header, payload, signature);
+  };
+
+  // Invalid token header.
+  {
+    const string token = create_token(
+        "NOT A VALID HEADER",
+        "{\"exp\":9999999999,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Invalid token payload.
+  {
+    const string token = create_token(
+        "{\"alg\":\"HS256\",\"typ\":\"JWT\"}",
+        "INVALID PAYLOAD");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Unsupported token alg.
+  {
+    const string token = create_token(
+        "{\"alg\":\"RS256\",\"typ\":\"JWT\"}",
+        "{\"exp\":9999999999,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Unknown token alg.
+  {
+    const string token = create_token(
+        "{\"alg\":\"NOT A VALID ALG\",\"typ\":\"JWT\"}",
+        "{\"exp\":9999999999,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // 'crit' in header.
+  {
+    const string token = create_token(
+        "{\"alg\":\"HS256\",\"crit\":[\"exp\"],\"exp\":99,\"typ\":\"JWT\"}",
+        "{\"exp\":9999999999,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Missing signature.
+  {
+    const string token =
+      base64::encode_url_safe("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", false) +
+      "." +
+      base64::encode_url_safe("{\"exp\":9999999999,\"sub\":\"foo\"}", false) +
+      ".";
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Wrong signature.
+  {
+    const string token =
+      base64::encode_url_safe("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", false) +
+      "." +
+      base64::encode_url_safe("{\"exp\":9999999999,\"sub\":\"foo\"}", false) +
+      "." +
+      "rm4sQe5q4sdHIJgt7mjKsnuZeP4eRquoZuncSsscqbQ";
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // 'none' alg with signature.
+  {
+    const string token = create_token(
+        "{\"alg\":\"none\",\"typ\":\"JWT\"}",
+        "{\"exp\":9999999999,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // 'none' alg missing dot.
+  {
+    const string token =
+      base64::encode_url_safe("{\"alg\":\"none\",\"typ\":\"JWT\"}", false) +
+      "." +
+      base64::encode_url_safe("{\"exp\":9999999999,\"sub\":\"foo\"}", false);
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Expiration date is not a number.
+  {
+    const string token = create_token(
+        "{\"alg\":\"HS256\",\"typ\":\"JWT\"}",
+        "{\"exp\":\"NOT A NUMBER\",\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Expiration date expired.
+  {
+    const string token = create_token(
+        "{\"alg\":\"HS256\",\"typ\":\"JWT\"}",
+        "{\"exp\":0,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    EXPECT_ERROR(jwt);
+  }
+
+  // Expiration date not set.
+  {
+    const string token = create_token(
+        "{\"alg\":\"HS256\",\"typ\":\"JWT\"}",
+        "{\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+    // once MESOS-7220 is resolved.
+    EXPECT_TRUE(jwt.isSome());
+  }
+
+  // Valid unsecure token.
+  {
+    const string token =
+      base64::encode_url_safe("{\"alg\":\"none\",\"typ\":\"JWT\"}", false) +
+      "." +
+      base64::encode_url_safe("{\"exp\":9999999999,\"sub\":\"foo\"}", false) +
+      ".";
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token);
+
+    // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+    // once MESOS-7220 is resolved.
+    EXPECT_TRUE(jwt.isSome());
+  }
+
+  // Valid HS256 token.
+  {
+    const string token = create_token(
+        "{\"alg\":\"HS256\",\"typ\":\"JWT\"}",
+        "{\"exp\":9999999999,\"sub\":\"foo\"}");
+
+    const Try<JWT, JWTError> jwt = JWT::parse(token, secret);
+
+    // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+    // once MESOS-7220 is resolved.
+    EXPECT_TRUE(jwt.isSome());
+  }
+}
+
+
+TEST(JWTTest, Create)
+{
+  const string secret = "secret";
+
+  auto create_signature = [secret](const JSON::Object& payload) {
+    const string message = strings::join(
+        ".",
+        base64::encode_url_safe("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", false),
+        base64::encode_url_safe(stringify(payload), false));
+
+    const string mac = generate_hmac_sha256(message, secret).get();
+
+    return base64::encode_url_safe(mac, false);
+  };
+
+  JSON::Object payload;
+  payload.values["exp"] = 9999999999;
+  payload.values["sub"] = "foo";
+
+  // HS256 signed JWT.
+  {
+    const 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());
+
+    EXPECT_EQ(JWT::Alg::HS256, jwt->header.alg);
+    EXPECT_SOME_EQ("JWT", jwt->header.typ);
+
+    EXPECT_EQ(payload, jwt->payload);
+
+    EXPECT_SOME_EQ(create_signature(payload), jwt->signature);
+  }
+
+  // Unsecured JWT.
+  {
+    const Try<JWT, JWTError> jwt = JWT::create(payload);
+
+    // TODO(nfnt): Change this to `EXPECT_SOME(jwt)`
+    // once MESOS-7220 is resolved.
+    EXPECT_TRUE(jwt.isSome());
+
+    EXPECT_EQ(JWT::Alg::None, jwt->header.alg);
+    EXPECT_SOME_EQ("JWT", jwt->header.typ);
+
+    EXPECT_EQ(payload, jwt->payload);
+
+    EXPECT_NONE(jwt->signature);
+  }
+}
+
+
+TEST(JWTTest, Stringify)
+{
+  JSON::Object payload;
+  payload.values["exp"] = 9999999999;
+  payload.values["sub"] = "foo";
+
+  const 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());
+
+  const string token = stringify(jwt.get());
+
+  EXPECT_EQ(
+      "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+      "eyJleHAiOjk5OTk5OTk5OTksInN1YiI6ImZvbyJ9."
+      "7dwSK1mIRKqJTPQT8-AGnI-r8nnefw2hhai3kgBg7bs",
+      token);
+}


[4/6] mesos git commit: Implemented the JWT authenticator.

Posted by vi...@apache.org.
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 {};