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 <j...@mesosphere.io>
Authored: Mon Mar 13 16:45:59 2017 +0100
Committer: Vinod Kone <vinodk...@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);
+}

Reply via email to