You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by tn...@apache.org on 2015/09/09 20:11:50 UTC

mesos git commit: Added TokenManager for Docker provisioner remote store.

Repository: mesos
Updated Branches:
  refs/heads/master 5c8065e9a -> b72fb052b


Added TokenManager for Docker provisioner remote store.

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


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

Branch: refs/heads/master
Commit: b72fb052bb2ca9f754b9d2c40e7f76df97dc55ae
Parents: 5c8065e
Author: Jojy Varghese <jo...@mesosphere.io>
Authored: Wed Sep 9 10:29:28 2015 -0700
Committer: Timothy Chen <tn...@apache.org>
Committed: Wed Sep 9 11:10:04 2015 -0700

----------------------------------------------------------------------
 src/Makefile.am                                 |   9 +-
 .../provisioners/docker/token_manager.cpp       | 361 +++++++++++++++++++
 .../provisioners/docker/token_manager.hpp       | 179 +++++++++
 .../provisioners/docker_provisioner_tests.cpp   | 354 ++++++++++++++++++
 4 files changed, 900 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/Makefile.am
----------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index 0a8ef6d..4ef58cd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -483,12 +483,13 @@ libmesos_no_3rdparty_la_SOURCES =					\
 	slave/containerizer/mesos/launch.cpp				\
 	slave/containerizer/provisioner.cpp				\
 	slave/containerizer/provisioners/appc.cpp			\
-        slave/containerizer/provisioners/paths.cpp                      \
+	slave/containerizer/provisioners/paths.cpp                      \
 	slave/containerizer/provisioners/appc/paths.cpp			\
 	slave/containerizer/provisioners/appc/spec.cpp			\
 	slave/containerizer/provisioners/appc/store.cpp			\
 	slave/containerizer/provisioners/backend.cpp			\
-        slave/containerizer/provisioners/backends/copy.cpp              \
+	slave/containerizer/provisioners/backends/copy.cpp              \
+	slave/containerizer/provisioners/docker/token_manager.cpp	\
 	slave/resource_estimators/noop.cpp				\
 	usage/usage.cpp							\
 	v1/attributes.cpp						\
@@ -763,13 +764,14 @@ libmesos_no_3rdparty_la_SOURCES +=					\
 	slave/containerizer/linux_launcher.hpp				\
 	slave/containerizer/provisioner.hpp				\
 	slave/containerizer/provisioners/appc.hpp			\
-        slave/containerizer/provisioners/paths.hpp                      \
+	slave/containerizer/provisioners/paths.hpp                      \
 	slave/containerizer/provisioners/appc/paths.hpp			\
 	slave/containerizer/provisioners/appc/spec.hpp			\
 	slave/containerizer/provisioners/appc/store.hpp			\
 	slave/containerizer/provisioners/backend.hpp			\
 	slave/containerizer/provisioners/backends/bind.hpp		\
 	slave/containerizer/provisioners/backends/copy.hpp		\
+	slave/containerizer/provisioners/docker/token_manager.hpp	\
 	slave/containerizer/isolators/posix.hpp				\
 	slave/containerizer/isolators/posix/disk.hpp			\
 	slave/containerizer/isolators/cgroups/constants.hpp		\
@@ -1659,6 +1661,7 @@ mesos_tests_SOURCES =						\
   tests/paths_tests.cpp						\
   tests/persistent_volume_tests.cpp				\
   tests/protobuf_io_tests.cpp					\
+  tests/provisioners/docker_provisioner_tests.cpp		\
   tests/rate_limiting_tests.cpp					\
   tests/reconciliation_tests.cpp				\
   tests/registrar_tests.cpp					\

http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/slave/containerizer/provisioners/docker/token_manager.cpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/provisioners/docker/token_manager.cpp b/src/slave/containerizer/provisioners/docker/token_manager.cpp
new file mode 100644
index 0000000..aec915f
--- /dev/null
+++ b/src/slave/containerizer/provisioners/docker/token_manager.cpp
@@ -0,0 +1,361 @@
+/**
+ * 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 <process/defer.hpp>
+#include <process/dispatch.hpp>
+
+#include "slave/containerizer/provisioners/docker/token_manager.hpp"
+
+using std::hash;
+using std::string;
+using std::vector;
+
+using process::Clock;
+using process::Failure;
+using process::Future;
+using process::Owned;
+using process::Process;
+using process::Time;
+
+using process::http::Request;
+using process::http::Response;
+using process::http::URL;
+
+namespace mesos {
+namespace internal {
+namespace slave {
+namespace docker {
+namespace registry {
+
+class TokenManagerProcess : public Process<TokenManagerProcess>
+{
+public:
+  static Try<Owned<TokenManagerProcess>> create(const URL& realm);
+
+  Future<Token> getToken(
+      const string& service,
+      const string& scope,
+      const Option<string>& account);
+
+private:
+  static const string TOKEN_PATH_PREFIX;
+  static const Duration RESPONSE_TIMEOUT;
+
+  TokenManagerProcess(const URL& realm)
+    : realm_(realm) {}
+
+  Try<Token> getTokenFromResponse(const Response& response) const;
+
+  /**
+   * Key for the token cache.
+   */
+  struct TokenCacheKey
+  {
+    string service;
+    string scope;
+  };
+
+  struct TokenCacheKeyHash
+  {
+    size_t operator()(const TokenCacheKey& key) const
+    {
+      hash<string> hashFn;
+
+      return (hashFn(key.service) ^
+          (hashFn(key.scope) << 1));
+    }
+  };
+
+  struct TokenCacheKeyEqual
+  {
+    bool operator()(
+        const TokenCacheKey& left,
+        const TokenCacheKey& right) const
+    {
+      return ((left.service == right.service) &&
+          (left.scope == right.scope));
+    }
+  };
+
+  typedef hashmap<
+    const TokenCacheKey,
+    Token,
+    TokenCacheKeyHash,
+    TokenCacheKeyEqual> TokenCacheType;
+
+  const URL realm_;
+  TokenCacheType tokenCache_;
+
+  TokenManagerProcess(const TokenManagerProcess&) = delete;
+  TokenManagerProcess& operator=(const TokenManagerProcess&) = delete;
+};
+
+const Duration TokenManagerProcess::RESPONSE_TIMEOUT = Seconds(10);
+const string TokenManagerProcess::TOKEN_PATH_PREFIX = "/v2/token/";
+
+
+Token::Token(
+    const string& _raw,
+    const JSON::Object& _header,
+    const JSON::Object& _claims,
+    const Option<Time>& _expiration,
+    const Option<Time>& _notBefore)
+  : raw(_raw),
+    header(_header),
+    claims(_claims),
+    expiration(_expiration),
+    notBefore(_notBefore) {}
+
+
+Try<Token> Token::create(const string& raw)
+{
+  auto decode = [](
+      const string& segment) -> Try<JSON::Object> {
+    const auto padding = segment.length() % 4;
+    string paddedSegment(segment);
+
+    if (padding) {
+      paddedSegment.append(padding, '=');
+    }
+
+    Try<string> decoded = base64::decode(paddedSegment);
+    if (decoded.isError()) {
+      return Error(decoded.error());
+    }
+
+    return JSON::parse<JSON::Object>(decoded.get());
+  };
+
+  const vector<string> tokens = strings::tokenize(raw, ".");
+
+  if (tokens.size() != 3) {
+    return Error("Invalid raw token string");
+  }
+
+  Try<JSON::Object> header = decode(tokens[0]);
+  if (header.isError()) {
+    return Error("Failed to decode 'header' segment: " + header.error());
+  }
+
+  Try<JSON::Object> claims = decode(tokens[1]);
+  if (claims.isError()) {
+    return Error("Failed to decode 'claims' segment: " + claims.error());
+  }
+
+  Result<Time> expirationTime = getTimeValue(claims.get(), "exp");
+  if (expirationTime.isError()) {
+    return Error("Failed to decode expiration time: " + expirationTime.error());
+  }
+
+  Option<Time> expiration;
+  if (expirationTime.isSome()) {
+    expiration = expirationTime.get();
+  }
+
+  Result<Time> notBeforeTime = getTimeValue(claims.get(), "nbf");
+  if (notBeforeTime.isError()) {
+    return Error("Failed to decode not-before time: " + notBeforeTime.error());
+  }
+
+  Option<Time> notBefore;
+  if (notBeforeTime.isSome()) {
+    notBefore = notBeforeTime.get();
+  }
+
+  Token token(raw, header.get(), claims.get(), expiration, notBefore);
+
+  if (token.isExpired()) {
+    return Error("Token has expired");
+  }
+
+  // TODO(jojy): Add signature validation.
+  return token;
+}
+
+
+Result<Time> Token::getTimeValue(const JSON::Object& object, const string& key)
+{
+  Result<JSON::Number> jsonValue = object.find<JSON::Number>(key);
+
+  Option<Time> timeValue;
+
+  // If expiration is provided, we will process it for future validations.
+  if (jsonValue.isSome()) {
+    Try<Time> time = Time::create(jsonValue.get().value);
+    if (time.isError()) {
+      return Error("Failed to decode time: " + time.error());
+    }
+
+    timeValue = time.get();
+  }
+
+  return timeValue;
+}
+
+
+bool Token::isExpired() const
+{
+  if (expiration.isSome()) {
+    return (Clock::now() >= expiration.get());
+  }
+
+  return false;
+}
+
+
+bool Token::isValid() const
+{
+  if (!isExpired()) {
+    if (notBefore.isSome()) {
+      return (Clock::now() >= notBefore.get());
+    }
+
+    return true;
+  }
+
+  // TODO(jojy): Add signature validation.
+  return false;
+}
+
+
+Try<Owned<TokenManager>> TokenManager::create(
+    const URL& realm)
+{
+  Try<Owned<TokenManagerProcess>> process = TokenManagerProcess::create(realm);
+  if (process.isError()) {
+    return Error(process.error());
+  }
+
+  return Owned<TokenManager>(new TokenManager(process.get()));
+}
+
+
+TokenManager::TokenManager(Owned<TokenManagerProcess>& process)
+  : process_(process)
+{
+  spawn(CHECK_NOTNULL(process_.get()));
+}
+
+
+TokenManager::~TokenManager()
+{
+  terminate(process_.get());
+  process::wait(process_.get());
+}
+
+
+Future<Token> TokenManager::getToken(
+    const string& service,
+    const string& scope,
+    const Option<string>& account)
+{
+  return dispatch(
+      process_.get(),
+      &TokenManagerProcess::getToken,
+      service,
+      scope,
+      account);
+}
+
+
+Try<Owned<TokenManagerProcess>> TokenManagerProcess::create(const URL& realm)
+{
+  return Owned<TokenManagerProcess>(new TokenManagerProcess(realm));
+}
+
+
+Try<Token> TokenManagerProcess::getTokenFromResponse(
+    const Response& response) const
+{
+  Try<JSON::Object> tokenJSON = JSON::parse<JSON::Object>(response.body);
+  if (tokenJSON.isError()) {
+    return Error(tokenJSON.error());
+  }
+
+  Result<JSON::String> tokenString =
+    tokenJSON.get().find<JSON::String>("token");
+
+  if (tokenString.isError()) {
+    return Error(tokenString.error());
+  }
+
+  Try<Token> result = Token::create(tokenString.get().value);
+  if (result.isError()) {
+    return Error(result.error());
+  }
+
+  return result.get();;
+}
+
+
+Future<Token> TokenManagerProcess::getToken(
+    const string& service,
+    const string& scope,
+    const Option<string>& account)
+{
+  const TokenCacheKey tokenKey = {service, scope};
+
+  if (tokenCache_.contains(tokenKey)) {
+    Token token = tokenCache_.at(tokenKey);
+
+    if (token.isValid()) {
+      return token;
+    } else {
+      LOG(WARNING) << "Cached token was invalid. Will fetch once again";
+    }
+  }
+
+  URL tokenUrl = realm_;
+  tokenUrl.path = TOKEN_PATH_PREFIX;
+
+  tokenUrl.query = {
+    {"service", service},
+    {"scope", scope},
+  };
+
+  if (account.isSome()) {
+    tokenUrl.query.insert({"account", account.get()});
+  }
+
+  return process::http::get(tokenUrl, None())
+    .after(RESPONSE_TIMEOUT, [] (Future<Response> resp) -> Future<Response> {
+      resp.discard();
+      return Failure("Timeout waiting for response to token request");
+    })
+    .then(defer(self(), [this, tokenKey](
+        const Future<Response>& response) -> Future<Token> {
+      Try<Token> token = getTokenFromResponse(response.get());
+      if (token.isError()) {
+        return Failure(
+            "Failed to parse JSON Web Token object from response: " +
+            token.error());
+      }
+
+      tokenCache_.insert({tokenKey, token.get()});
+
+      return token.get();
+    }));
+}
+
+// TODO(jojy): Add implementation for basic authentication based getToken API.
+
+} // namespace registry {
+} // namespace docker {
+} // namespace slave {
+} // namespace internal {
+} // namespace mesos {

http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/slave/containerizer/provisioners/docker/token_manager.hpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/provisioners/docker/token_manager.hpp b/src/slave/containerizer/provisioners/docker/token_manager.hpp
new file mode 100644
index 0000000..879269d
--- /dev/null
+++ b/src/slave/containerizer/provisioners/docker/token_manager.hpp
@@ -0,0 +1,179 @@
+/**
+ * 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 __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__
+#define __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__
+
+#include <functional>
+#include <string>
+
+#include <stout/base64.hpp>
+#include <stout/duration.hpp>
+#include <stout/hashmap.hpp>
+#include <stout/strings.hpp>
+
+#include <process/future.hpp>
+#include <process/http.hpp>
+#include <process/process.hpp>
+#include <process/time.hpp>
+
+namespace mesos {
+namespace internal {
+namespace slave {
+namespace docker {
+namespace registry {
+
+
+/**
+ * Encapsulates JSON Web Token.
+ *
+ * Reference: https://tools.ietf.org/html/rfc7519.
+ */
+struct Token
+{
+  /**
+   * Factory method for Token object.
+   *
+   * Parses the raw token string and validates for token's expiration.
+   *
+   * @returns Token if parsing and validation succeeds.
+   *          Error if parsing or validation fails.
+   */
+  static Try<Token> create(const std::string& rawString);
+
+  /**
+   * Compares token's expiration time(expressed in seconds) with current time.
+   *
+   * @returns True if token's expiration time is greater than current time.
+   *          False if token's expiration time is less than or equal to current
+   *          time.
+   */
+  bool isExpired() const;
+
+  /**
+   * Validates the token if its "exp" "nbf" values are in range.
+   *
+   * @returns True if current time is within token's "exp" and "nbf" values.
+   *          False if current time is not within token's "exp" and "nbf"
+   *          values.
+   */
+  bool isValid() const;
+
+  const std::string raw;
+  const JSON::Object header;
+  const JSON::Object claims;
+  // TODO(jojy): Add signature information.
+
+private:
+  Token(
+      const std::string& raw,
+      const JSON::Object& headerJson,
+      const JSON::Object& claimsJson,
+      const Option<process::Time>& expireTime,
+      const Option<process::Time>& notBeforeTime);
+
+  static Result<process::Time> getTimeValue(
+      const JSON::Object& object,
+      const std::string& key);
+
+  const Option<process::Time> expiration;
+  const Option<process::Time> notBefore;
+};
+
+
+// Forward declaration.
+class TokenManagerProcess;
+
+
+/**
+ *  Acquires and manages docker registry tokens. It keeps the tokens in its
+ *  cache to server any future request for the same token.
+ *  The cache grows unbounded.
+ *  TODO(jojy): The cache can be optimized to prune based on the expiry time of
+ *  the token and server's issue time.
+ */
+class TokenManager
+{
+public:
+  /**
+   * Factory method for creating TokenManager object.
+   *
+   * TokenManager and registry authorization realm has a 1:1 relationship.
+   *
+   * @param realm URL of the authorization server from where token will be
+   *     requested by this TokenManager.
+   * @returns Owned<TokenManager> if success.
+   *          Error on failure.
+   */
+  static Try<process::Owned<TokenManager>> create(
+      const process::http::URL& realm);
+
+  /**
+   * Returns JSON Web Token from cache or from remote server using "Basic
+   * authorization".
+   *
+   * @param service Name of the service that hosts the resource for which
+   *     token is being requested.
+   * @param scope unique scope returned by the 401 Unauthorized response
+   *     from the registry.
+   * @param account Name of the account which the client is acting as.
+   * @param user base64 encoded userid for basic authorization.
+   * @param password base64 encoded password for basic authorization.
+   * @returns Token struct that encapsulates JSON Web Token.
+   */
+  process::Future<Token> getToken(
+      const std::string& service,
+      const std::string& scope,
+      const Option<std::string>& account,
+      const std::string& user,
+      const Option<std::string>& password);
+
+  /**
+   * Returns JSON Web Token from cache or from remote server using "TLS/Cert"
+   * based authorization.
+   *
+   * @param service Name of the service that hosts the resource for which
+   *     token is being requested.
+   * @param scope unique scope returned by the 401 Unauthorized response
+   *     from the registry.
+   * @param account Name of the account which the client is acting as.
+   * @returns Token struct that encapsulates JSON Web Token.
+   */
+  process::Future<Token> getToken(
+      const std::string& service,
+      const std::string& scope,
+      const Option<std::string>& account);
+
+  ~TokenManager();
+
+private:
+  TokenManager(process::Owned<TokenManagerProcess>& process);
+
+  TokenManager(const TokenManager&) = delete;
+  TokenManager& operator=(const TokenManager&) = delete;
+
+  process::Owned<TokenManagerProcess> process_;
+};
+
+} // namespace registry {
+} // namespace docker {
+} // namespace slave {
+} // namespace internal {
+} // namespace mesos {
+
+#endif // __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/tests/provisioners/docker_provisioner_tests.cpp
----------------------------------------------------------------------
diff --git a/src/tests/provisioners/docker_provisioner_tests.cpp b/src/tests/provisioners/docker_provisioner_tests.cpp
new file mode 100644
index 0000000..ff29d56
--- /dev/null
+++ b/src/tests/provisioners/docker_provisioner_tests.cpp
@@ -0,0 +1,354 @@
+/**
+ * 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 <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <stout/duration.hpp>
+
+#include <process/address.hpp>
+#include <process/clock.hpp>
+#include <process/future.hpp>
+#include <process/gmock.hpp>
+#include <process/owned.hpp>
+#include <process/socket.hpp>
+#include <process/subprocess.hpp>
+
+#include <process/ssl/gtest.hpp>
+
+#include "slave/containerizer/provisioners/docker/token_manager.hpp"
+
+#include "tests/mesos.hpp"
+
+using std::map;
+using std::string;
+using std::vector;
+
+using namespace mesos::internal::slave::docker::registry;
+using namespace process;
+
+namespace mesos {
+namespace internal {
+namespace tests {
+
+
+/**
+ * Provides token operations and defaults.
+ */
+class TokenHelper {
+protected:
+  const string hdrBase64 = base64::encode(
+    "{ \
+      \"alg\":\"ES256\", \
+      \"typ\":\"JWT\", \
+      \"x5c\":[\"test\"] \
+    }");
+
+  string getClaimsBase64() const
+  {
+    return base64::encode(claimsJsonString);
+  }
+
+  string getTokenString() const
+  {
+    return  hdrBase64 + "." + getClaimsBase64() + "." + signBase64;
+  }
+
+  const string signBase64 = base64::encode("{\"\"}");
+  string claimsJsonString;
+};
+
+
+/**
+ * Fixture for testing TokenManager component.
+ */
+class DockerRegistryTokenTest : public TokenHelper, public ::testing::Test
+{};
+
+
+// Tests JSON Web Token parsing for a valid token string.
+TEST_F(DockerRegistryTokenTest, ValidToken)
+{
+  const double expirySecs = Clock::now().secs() + Days(365).secs();
+
+  claimsJsonString =
+    "{\"access\" \
+      :[ \
+        { \
+          \"type\":\"repository\", \
+          \"name\":\"library/busybox\", \
+          \"actions\":[\"pull\"]}], \
+          \"aud\":\"registry.docker.io\", \
+          \"exp\":" + stringify(expirySecs) + ", \
+          \"iat\":1438887168, \
+          \"iss\":\"auth.docker.io\", \
+          \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
+          \"nbf\":1438887166, \
+          \"sub\":\"\" \
+         }";
+
+  Try<Token> token = Token::create(getTokenString());
+
+  ASSERT_SOME(token);
+}
+
+
+// Tests JSON Web Token parsing for a token string with expiration date in the
+// past.
+TEST_F(DockerRegistryTokenTest, ExpiredToken)
+{
+  const double expirySecs = Clock::now().secs() - Days(365).secs();
+
+  claimsJsonString =
+    "{\"access\" \
+      :[ \
+        { \
+          \"type\":\"repository\", \
+          \"name\":\"library/busybox\", \
+          \"actions\":[\"pull\"]}], \
+          \"aud\":\"registry.docker.io\", \
+          \"exp\":" + stringify(expirySecs) + ", \
+          \"iat\":1438887166, \
+          \"iss\":\"auth.docker.io\", \
+          \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
+          \"nbf\":1438887166, \
+          \"sub\":\"\" \
+         }";
+
+  Try<Token> token = Token::create(getTokenString());
+
+  EXPECT_ERROR(token);
+}
+
+
+// Tests JSON Web Token parsing for a token string with no expiration date.
+TEST_F(DockerRegistryTokenTest, NoExpiration)
+{
+  claimsJsonString =
+    "{\"access\" \
+      :[ \
+        { \
+          \"type\":\"repository\", \
+          \"name\":\"library/busybox\", \
+          \"actions\":[\"pull\"]}], \
+          \"aud\":\"registry.docker.io\", \
+          \"iat\":1438887166, \
+          \"iss\":\"auth.docker.io\", \
+          \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
+          \"nbf\":1438887166, \
+          \"sub\":\"\" \
+      }";
+
+  const Try<Token> token = Token::create(getTokenString());
+
+  ASSERT_SOME(token);
+}
+
+
+// Tests JSON Web Token parsing for a token string with not-before date in the
+// future.
+TEST_F(DockerRegistryTokenTest, NotBeforeInFuture)
+{
+  const double expirySecs = Clock::now().secs() + Days(365).secs();
+  const double nbfSecs = Clock::now().secs() + Days(7).secs();
+
+  claimsJsonString =
+    "{\"access\" \
+      :[ \
+        { \
+          \"type\":\"repository\", \
+          \"name\":\"library/busybox\", \
+          \"actions\":[\"pull\"]}], \
+          \"aud\":\"registry.docker.io\", \
+          \"exp\":" + stringify(expirySecs) + ", \
+          \"iat\":1438887166, \
+          \"iss\":\"auth.docker.io\", \
+          \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
+          \"nbf\":" + stringify(nbfSecs) + ", \
+          \"sub\":\"\" \
+         }";
+
+  const Try<Token> token = Token::create(getTokenString());
+
+  ASSERT_SOME(token);
+  ASSERT_EQ(token.get().isValid(), false);
+}
+
+
+#ifdef USE_SSL_SOCKET
+
+// Test suite for docker registry tests.
+class DockerRegistryClientTest : public virtual SSLTest, public TokenHelper
+{
+protected:
+  DockerRegistryClientTest() {}
+
+  static void SetUpTestCase()
+  {
+    SSLTest::SetUpTestCase();
+    // TODO(jojy): Add registry specific directory setup. Will be added in the
+    // next patch when docker registry client tests are added.
+  }
+
+  static void TearDownTestCase()
+  {
+    SSLTest::TearDownTestCase();
+    // TODO(jojy): Add registry specific directory cleanup. Will be added in the
+    // next patch when docker registry client tests are added.
+  }
+};
+
+
+// Tests TokenManager for a simple token request.
+TEST_F(DockerRegistryClientTest, SimpleGetToken)
+{
+  Try<Socket> server = setup_server({
+      {"SSL_ENABLED", "true"},
+      {"SSL_KEY_FILE", key_path().value},
+      {"SSL_CERT_FILE", certificate_path().value}});
+
+  ASSERT_SOME(server);
+  ASSERT_SOME(server.get().address());
+  ASSERT_SOME(server.get().address().get().hostname());
+
+  Future<Socket> socket = server.get().accept();
+
+  // Create URL from server hostname and port.
+  const http::URL url(
+      "https",
+      server.get().address().get().hostname().get(),
+      server.get().address().get().port);
+
+  Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url);
+  ASSERT_SOME(tokenMgr);
+
+  Future<Token> token =
+    tokenMgr.get()->getToken(
+        "registry.docker.io",
+        "repository:library/busybox:pull",
+        None());
+
+  AWAIT_ASSERT_READY(socket);
+
+  // Construct response and send(server side).
+  const double expirySecs = Clock::now().secs() + Days(365).secs();
+
+  claimsJsonString =
+    "{\"access\" \
+      :[ \
+        { \
+          \"type\":\"repository\", \
+          \"name\":\"library/busybox\", \
+          \"actions\":[\"pull\"]}], \
+          \"aud\":\"registry.docker.io\", \
+          \"exp\":" + stringify(expirySecs) + ", \
+          \"iat\":1438887168, \
+          \"iss\":\"auth.docker.io\", \
+          \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
+          \"nbf\":1438887166, \
+          \"sub\":\"\" \
+         }";
+
+  const string tokenString(getTokenString());
+  const string tokenResponse = "{\"token\":\"" + tokenString + "\"}";
+
+  const string buffer =
+    string("HTTP/1.1 200 OK\r\n") +
+    "Content-Length : " +
+    stringify(tokenResponse.length()) + "\r\n" +
+    "\r\n" +
+    tokenResponse;
+
+  AWAIT_ASSERT_READY(Socket(socket.get()).send(buffer));
+
+  AWAIT_ASSERT_READY(token);
+  ASSERT_EQ(token.get().raw, tokenString);
+}
+
+
+// Tests TokenManager for bad token response from server.
+TEST_F(DockerRegistryClientTest, BadTokenResponse)
+{
+  Try<Socket> server = setup_server({
+      {"SSL_ENABLED", "true"},
+      {"SSL_KEY_FILE", key_path().value},
+      {"SSL_CERT_FILE", certificate_path().value}});
+
+  ASSERT_SOME(server);
+  ASSERT_SOME(server.get().address());
+  ASSERT_SOME(server.get().address().get().hostname());
+
+  Future<Socket> socket = server.get().accept();
+
+  // Create URL from server hostname and port.
+  const http::URL url(
+      "https",
+      server.get().address().get().hostname().get(),
+      server.get().address().get().port);
+
+  Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url);
+  ASSERT_SOME(tokenMgr);
+
+  Future<Token> token =
+    tokenMgr.get()->getToken(
+        "registry.docker.io",
+        "repository:library/busybox:pull",
+        None());
+
+  AWAIT_ASSERT_READY(socket);
+
+  const string tokenString("bad token");
+  const string tokenResponse = "{\"token\":\"" + tokenString + "\"}";
+
+  const string buffer =
+    string("HTTP/1.1 200 OK\r\n") +
+    "Content-Length : " +
+    stringify(tokenResponse.length()) + "\r\n" +
+    "\r\n" +
+    tokenResponse;
+
+  AWAIT_ASSERT_READY(Socket(socket.get()).send(buffer));
+
+  AWAIT_FAILED(token);
+}
+
+
+// Tests TokenManager for request to invalid server.
+TEST_F(DockerRegistryClientTest, BadTokenServerAddress)
+{
+  // Create an invalid URL with current time.
+  const http::URL url("https", stringify(Clock::now().secs()), 0);
+
+  Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url);
+  ASSERT_SOME(tokenMgr);
+
+  Future<Token> token =
+    tokenMgr.get()->getToken(
+        "registry.docker.io",
+        "repository:library/busybox:pull",
+        None());
+
+  AWAIT_FAILED(token);
+}
+
+#endif // USE_SSL_SOCKET
+
+
+} // namespace tests {
+} // namespace internal {
+} // namespace mesos {