Make App ID a part of User ID

In addition to user ID, auth tokens could be bound to specific app ID.
So internal libweave User ID, named UserAppId, from now will consist of
auth type, user ID and optional app ID. If operation was called with
token containing only user ID, libweave will grant access to all
commands for every app for the given user ID.

To distinguish between user authorized with local, pairing or anonymous
tokens libweave uses UserAppId::type field. As macaroons have no caveats
for this kind of information, current implementation will just append
the type to the user ID caveat of the access token.

BUG: 26292014

Change-Id: I528c2717c95c5daed74bb769b3569fac823761f2
Reviewed-on: https://weave-review.googlesource.com/2394
Reviewed-by: Alex Vakulenko <avakulenko@google.com>
diff --git a/src/privet/auth_manager.cc b/src/privet/auth_manager.cc
index 3c08071..0b8a981 100644
--- a/src/privet/auth_manager.cc
+++ b/src/privet/auth_manager.cc
@@ -96,16 +96,26 @@
 
 class UserIdCaveat : public Caveat {
  public:
-  explicit UserIdCaveat(const std::string& id)
+  explicit UserIdCaveat(const std::vector<uint8_t>& id)
       : Caveat(kUwMacaroonCaveatTypeDelegateeUser, id.size()) {
     CHECK(uw_macaroon_caveat_create_delegatee_user_(
-        reinterpret_cast<const uint8_t*>(id.data()), id.size(), buffer_.data(),
-        buffer_.size(), &caveat_));
+        id.data(), id.size(), buffer_.data(), buffer_.size(), &caveat_));
   }
 
   DISALLOW_COPY_AND_ASSIGN(UserIdCaveat);
 };
 
+class AppIdCaveat : public Caveat {
+ public:
+  explicit AppIdCaveat(const std::vector<uint8_t>& id)
+      : Caveat(kUwMacaroonCaveatTypeDelegateeApp, id.size()) {
+    CHECK(uw_macaroon_caveat_create_delegatee_app_(
+        id.data(), id.size(), buffer_.data(), buffer_.size(), &caveat_));
+  }
+
+  DISALLOW_COPY_AND_ASSIGN(AppIdCaveat);
+};
+
 class ServiceCaveat : public Caveat {
  public:
   explicit ServiceCaveat(const std::string& id)
@@ -312,14 +322,19 @@
 std::vector<uint8_t> AuthManager::CreateAccessToken(const UserInfo& user_info,
                                                     base::TimeDelta ttl) const {
   ScopeCaveat scope{ToMacaroonScope(user_info.scope())};
-  UserIdCaveat user{user_info.user_id()};
+  // Macaroons have no caveats for auth type. So we just append the type to the
+  // user ID.
+  std::vector<uint8_t> id_with_type{user_info.id().user};
+  id_with_type.push_back(static_cast<uint8_t>(user_info.id().type));
+  UserIdCaveat user{id_with_type};
+  AppIdCaveat app{user_info.id().app};
   const base::Time now = Now();
   ExpirationCaveat expiration{now + ttl};
-  return CreateMacaroonToken(
-      access_secret_, now,
-      {
-          &scope.GetCaveat(), &user.GetCaveat(), &expiration.GetCaveat(),
-      });
+  return CreateMacaroonToken(access_secret_, now,
+                             {
+                                 &scope.GetCaveat(), &user.GetCaveat(),
+                                 &app.GetCaveat(), &expiration.GetCaveat(),
+                             });
 }
 
 bool AuthManager::ParseAccessToken(const std::vector<uint8_t>& token,
@@ -331,7 +346,7 @@
   UwMacaroonValidationResult result{};
   const base::Time now = Now();
   if (!LoadMacaroon(token, &buffer, &macaroon, error) ||
-      macaroon.num_caveats != 3 ||
+      macaroon.num_caveats != 4 ||
       !VerifyMacaroon(access_secret_, macaroon, now, &result, error)) {
     return Error::AddTo(error, FROM_HERE, errors::kInvalidAuthorization,
                         "Invalid token");
@@ -346,12 +361,22 @@
   // If token is valid and token was not extended, it should has precisely this
   // values.
   CHECK_GE(FromJ2000Time(result.expiration_time), now);
-  CHECK_EQ(1u, result.num_delegatees);
+  CHECK_EQ(2u, result.num_delegatees);
   CHECK_EQ(kUwMacaroonDelegateeTypeUser, result.delegatees[0].type);
-  std::string user_id{reinterpret_cast<const char*>(result.delegatees[0].id),
-                      result.delegatees[0].id_len};
+  CHECK_EQ(kUwMacaroonDelegateeTypeApp, result.delegatees[1].type);
+  CHECK_GT(result.delegatees[0].id_len, 1u);
+  std::vector<uint8_t> user_id{
+      result.delegatees[0].id,
+      result.delegatees[0].id + result.delegatees[0].id_len};
+  // Last byte is used for type. See |CreateAccessToken|.
+  AuthType type = static_cast<AuthType>(user_id.back());
+  user_id.pop_back();
+
+  std::vector<uint8_t> app_id{
+      result.delegatees[1].id,
+      result.delegatees[1].id + result.delegatees[1].id_len};
   if (user_info)
-    *user_info = UserInfo{auth_scope, user_id};
+    *user_info = UserInfo{auth_scope, UserAppId{type, user_id, app_id}};
 
   return true;
 }
@@ -463,6 +488,11 @@
                    [](const UwMacaroonDelegateeInfo& delegatee) {
                      return delegatee.type == kUwMacaroonDelegateeTypeUser;
                    });
+  auto last_app_id =
+      std::find_if(delegates_rbegin, delegates_rend,
+                   [](const UwMacaroonDelegateeInfo& delegatee) {
+                     return delegatee.type == kUwMacaroonDelegateeTypeApp;
+                   });
 
   if (last_user_id == delegates_rend || !last_user_id->id_len) {
     return Error::AddTo(error, FROM_HERE, errors::kInvalidAuthCode,
@@ -480,9 +510,13 @@
   if (!access_token)
     return true;
 
-  std::string user_id{reinterpret_cast<const char*>(last_user_id->id),
-                      last_user_id->id_len};
-  UserInfo info{auth_scope, user_id};
+  std::vector<uint8_t> user_id{last_user_id->id,
+                               last_user_id->id + last_user_id->id_len};
+  std::vector<uint8_t> app_id;
+  if (last_app_id != delegates_rend)
+    app_id.assign(last_app_id->id, last_app_id->id + last_app_id->id_len);
+
+  UserInfo info{auth_scope, {AuthType::kLocal, user_id, app_id}};
 
   ttl = std::min(ttl, FromJ2000Time(result.expiration_time) - now);
   *access_token = CreateAccessToken(info, ttl);
@@ -519,15 +553,21 @@
   TimestampCaveat issued{now};
   ExpirationCaveat expiration{now + ttl};
   ScopeCaveat scope{ToMacaroonScope(user_info.scope())};
-  UserIdCaveat user{user_info.user_id()};
+  UserIdCaveat user{user_info.id().user};
+  AppIdCaveat app{user_info.id().app};
   SessionIdCaveat session{CreateSessionId()};
 
-  return ExtendMacaroonToken(
-      macaroon, now,
-      {
-          &issued.GetCaveat(), &expiration.GetCaveat(), &scope.GetCaveat(),
-          &user.GetCaveat(), &session.GetCaveat(),
-      });
+  std::vector<const UwMacaroonCaveat*> caveats{
+      &issued.GetCaveat(), &expiration.GetCaveat(), &scope.GetCaveat(),
+      &user.GetCaveat(),
+  };
+
+  if (!user_info.id().app.empty())
+    caveats.push_back(&app.GetCaveat());
+
+  caveats.push_back(&session.GetCaveat());
+
+  return ExtendMacaroonToken(macaroon, now, caveats);
 }
 
 }  // namespace privet
diff --git a/src/privet/auth_manager_unittest.cc b/src/privet/auth_manager_unittest.cc
index d88d033..a0a0d01 100644
--- a/src/privet/auth_manager_unittest.cc
+++ b/src/privet/auth_manager_unittest.cc
@@ -10,6 +10,7 @@
 
 #include "src/config.h"
 #include "src/data_encoding.h"
+#include "src/privet/mock_delegates.h"
 #include "src/test/mock_clock.h"
 
 using testing::Return;
@@ -69,49 +70,90 @@
 }
 
 TEST_F(AuthManagerTest, CreateAccessToken) {
-  EXPECT_EQ("WCKDQgEURQlDMjM0RgUaG52hAFA3hFh7TexW1jC96sU4CxvN",
+  EXPECT_EQ("WCaEQgEURglEMjM0AEIKQEYFGhudoQBQaML7svgzFKDXI+/geUUn0w==",
             Base64Encode(auth_.CreateAccessToken(
-                UserInfo{AuthScope::kViewer, "234"}, {})));
-  EXPECT_EQ("WCKDQgEIRQlDMjU3RgUaG52hAFD3dEHl3Y9Y28uoUESiYuLq",
+                UserInfo{AuthScope::kViewer, TestUserId{"234"}}, {})));
+  EXPECT_EQ("WCaEQgEIRglEMjU3AEIKQEYFGhudoQBQ0f4NfEW7KDC1QnbExFbf9w==",
             Base64Encode(auth_.CreateAccessToken(
-                UserInfo{AuthScope::kManager, "257"}, {})));
-  EXPECT_EQ("WCKDQgECRQlDNDU2RgUaG52hAFBy35bQdtvlqYf+Y/ANyxLU",
+                UserInfo{AuthScope::kManager, TestUserId{"257"}}, {})));
+  EXPECT_EQ("WCaEQgECRglENDU2AEIKQEYFGhudoQBQtgk1ZlsGqs5gF7m+UpwxmQ==",
             Base64Encode(auth_.CreateAccessToken(
-                UserInfo{AuthScope::kOwner, "456"}, {})));
+                UserInfo{AuthScope::kOwner, TestUserId{"456"}}, {})));
   auto new_time = clock_.Now() + base::TimeDelta::FromDays(11);
   EXPECT_CALL(clock_, Now()).WillRepeatedly(Return(new_time));
-  EXPECT_EQ("WCKDQgEORQlDMzQ1RgUaG6whgFD1HGVxL8+FPaf/U0bOkXr8",
+  EXPECT_EQ("WCaEQgEORglEMzQ1AEIKQEYFGhusIYBQl3SG1De+fl2qTquwTl1uRA==",
             Base64Encode(auth_.CreateAccessToken(
-                UserInfo{AuthScope::kUser, "345"}, {})));
+                UserInfo{AuthScope::kUser, TestUserId{"345"}}, {})));
 }
 
 TEST_F(AuthManagerTest, CreateSameToken) {
-  EXPECT_EQ(auth_.CreateAccessToken(UserInfo{AuthScope::kViewer, "555"}, {}),
-            auth_.CreateAccessToken(UserInfo{AuthScope::kViewer, "555"}, {}));
+  EXPECT_EQ(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer, TestUserId{"555"}}, {}),
+            auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer, TestUserId{"555"}}, {}));
+}
+
+TEST_F(AuthManagerTest, CreateSameTokenWithApp) {
+  EXPECT_EQ(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer,
+                         {AuthType::kLocal, {1, 2, 3}, {4, 5, 6}}},
+                {}),
+            auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer,
+                         {AuthType::kLocal, {1, 2, 3}, {4, 5, 6}}},
+                {}));
+}
+
+TEST_F(AuthManagerTest, CreateSameTokenWithDifferentType) {
+  EXPECT_NE(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer,
+                         {AuthType::kLocal, {1, 2, 3}, {4, 5, 6}}},
+                {}),
+            auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer,
+                         {AuthType::kPairing, {1, 2, 3}, {4, 5, 6}}},
+                {}));
+}
+
+TEST_F(AuthManagerTest, CreateSameTokenWithDifferentApp) {
+  EXPECT_NE(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer,
+                         {AuthType::kLocal, {1, 2, 3}, {4, 5, 6}}},
+                {}),
+            auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer,
+                         {AuthType::kLocal, {1, 2, 3}, {4, 5, 7}}},
+                {}));
 }
 
 TEST_F(AuthManagerTest, CreateTokenDifferentScope) {
-  EXPECT_NE(auth_.CreateAccessToken(UserInfo{AuthScope::kViewer, "456"}, {}),
-            auth_.CreateAccessToken(UserInfo{AuthScope::kOwner, "456"}, {}));
+  EXPECT_NE(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kViewer, TestUserId{"456"}}, {}),
+            auth_.CreateAccessToken(
+                UserInfo{AuthScope::kOwner, TestUserId{"456"}}, {}));
 }
 
 TEST_F(AuthManagerTest, CreateTokenDifferentUser) {
-  EXPECT_NE(auth_.CreateAccessToken(UserInfo{AuthScope::kOwner, "456"}, {}),
-            auth_.CreateAccessToken(UserInfo{AuthScope::kOwner, "789"}, {}));
+  EXPECT_NE(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kOwner, TestUserId{"456"}}, {}),
+            auth_.CreateAccessToken(
+                UserInfo{AuthScope::kOwner, TestUserId{"789"}}, {}));
 }
 
 TEST_F(AuthManagerTest, CreateTokenDifferentTime) {
-  auto token = auth_.CreateAccessToken(UserInfo{AuthScope::kOwner, "567"}, {});
+  auto token = auth_.CreateAccessToken(
+      UserInfo{AuthScope::kOwner, TestUserId{"567"}}, {});
   EXPECT_CALL(clock_, Now())
       .WillRepeatedly(Return(base::Time::FromTimeT(1400000000)));
-  EXPECT_NE(token,
-            auth_.CreateAccessToken(UserInfo{AuthScope::kOwner, "567"}, {}));
+  EXPECT_NE(token, auth_.CreateAccessToken(
+                       UserInfo{AuthScope::kOwner, TestUserId{"567"}}, {}));
 }
 
 TEST_F(AuthManagerTest, CreateTokenDifferentInstance) {
-  EXPECT_NE(auth_.CreateAccessToken(UserInfo{AuthScope::kUser, "123"}, {}),
+  EXPECT_NE(auth_.CreateAccessToken(
+                UserInfo{AuthScope::kUser, TestUserId{"123"}}, {}),
             AuthManager({}, {}).CreateAccessToken(
-                UserInfo{AuthScope::kUser, "123"}, {}));
+                UserInfo{AuthScope::kUser, TestUserId{"123"}}, {}));
 }
 
 TEST_F(AuthManagerTest, ParseAccessToken) {
@@ -122,20 +164,22 @@
 
     AuthManager auth{{}, {}, {}, &clock_};
 
-    auto token = auth.CreateAccessToken(UserInfo{AuthScope::kUser, "5"},
-                                        base::TimeDelta::FromSeconds(i));
+    auto token =
+        auth.CreateAccessToken(UserInfo{AuthScope::kUser, TestUserId{"5"}},
+                               base::TimeDelta::FromSeconds(i));
     UserInfo user_info;
     EXPECT_FALSE(auth_.ParseAccessToken(token, &user_info, nullptr));
     EXPECT_TRUE(auth.ParseAccessToken(token, &user_info, nullptr));
     EXPECT_EQ(AuthScope::kUser, user_info.scope());
-    EXPECT_EQ("5", user_info.user_id());
+    EXPECT_EQ(TestUserId{"5"}, user_info.id());
 
     EXPECT_CALL(clock_, Now())
         .WillRepeatedly(Return(kStartTime + base::TimeDelta::FromSeconds(i)));
     EXPECT_TRUE(auth.ParseAccessToken(token, &user_info, nullptr));
 
-    auto extended = DelegateToUser(token, base::TimeDelta::FromSeconds(1000),
-                                   UserInfo{AuthScope::kUser, "234"});
+    auto extended =
+        DelegateToUser(token, base::TimeDelta::FromSeconds(1000),
+                       UserInfo{AuthScope::kUser, TestUserId{"234"}});
     EXPECT_FALSE(auth.ParseAccessToken(extended, &user_info, nullptr));
 
     EXPECT_CALL(clock_, Now())
@@ -207,7 +251,7 @@
   base::TimeDelta ttl;
   auto root = auth_.GetRootClientAuthToken(RootClientTokenOwner::kCloud);
   auto extended = DelegateToUser(root, base::TimeDelta::FromSeconds(1000),
-                                 UserInfo{AuthScope::kUser, "234"});
+                                 UserInfo{AuthScope::kUser, TestUserId{"234"}});
   EXPECT_EQ(
       "WE+IQxkgAUYIGhudoQBMDEpnb29nbGUuY29tRggaG52hAEYFGhudpOhCAQ5FCUMyMzRNEUs0"
       "NjMzMTUyMDA6MVCRVKU+0SpOoBppnwqdKMwP",
@@ -220,7 +264,7 @@
   EXPECT_EQ(scope, user_info.scope());
   EXPECT_EQ(AuthScope::kUser, user_info.scope());
 
-  EXPECT_EQ("234", user_info.user_id());
+  EXPECT_EQ(TestUserId{"234"}, user_info.id());
 }
 
 TEST_F(AuthManagerTest, CreateAccessTokenFromAuthNotMinted) {
@@ -235,7 +279,7 @@
 TEST_F(AuthManagerTest, CreateAccessTokenFromAuthValidateAfterSomeTime) {
   auto root = auth_.GetRootClientAuthToken(RootClientTokenOwner::kClient);
   auto extended = DelegateToUser(root, base::TimeDelta::FromSeconds(1000),
-                                 UserInfo{AuthScope::kUser, "234"});
+                                 UserInfo{AuthScope::kUser, TestUserId{"234"}});
 
   // new_time < session_id_expiration < token_expiration.
   auto new_time = clock_.Now() + base::TimeDelta::FromSeconds(15);
@@ -248,7 +292,7 @@
 TEST_F(AuthManagerTest, CreateAccessTokenFromAuthExpired) {
   auto root = auth_.GetRootClientAuthToken(RootClientTokenOwner::kClient);
   auto extended = DelegateToUser(root, base::TimeDelta::FromSeconds(10),
-                                 UserInfo{AuthScope::kUser, "234"});
+                                 UserInfo{AuthScope::kUser, TestUserId{"234"}});
   ErrorPtr error;
 
   // token_expiration < new_time < session_id_expiration.
@@ -263,7 +307,7 @@
 TEST_F(AuthManagerTest, CreateAccessTokenFromAuthExpiredSessionid) {
   auto root = auth_.GetRootClientAuthToken(RootClientTokenOwner::kClient);
   auto extended = DelegateToUser(root, base::TimeDelta::FromSeconds(1000),
-                                 UserInfo{AuthScope::kUser, "234"});
+                                 UserInfo{AuthScope::kUser, TestUserId{"234"}});
   ErrorPtr error;
 
   // session_id_expiration < new_time < token_expiration.
diff --git a/src/privet/cloud_delegate.cc b/src/privet/cloud_delegate.cc
index 5f31fee..49fceaa 100644
--- a/src/privet/cloud_delegate.cc
+++ b/src/privet/cloud_delegate.cc
@@ -165,7 +165,7 @@
                   const UserInfo& user_info,
                   const CommandDoneCallback& callback) override {
     CHECK(user_info.scope() != AuthScope::kNone);
-    CHECK(!user_info.user_id().empty());
+    CHECK(!user_info.id().IsEmpty());
 
     ErrorPtr error;
     UserRole role;
@@ -182,7 +182,7 @@
     if (!command_instance)
       return callback.Run({}, std::move(error));
     component_manager_->AddCommand(std::move(command_instance));
-    command_owners_[id] = user_info.user_id();
+    command_owners_[id] = user_info.id();
     callback.Run(*component_manager_->FindCommand(id)->ToJson(), nullptr);
   }
 
@@ -230,7 +230,7 @@
  private:
   void OnCommandAdded(Command* command) {
     // Set to "" for any new unknown command.
-    command_owners_.insert(std::make_pair(command->GetID(), ""));
+    command_owners_.insert(std::make_pair(command->GetID(), UserAppId{}));
   }
 
   void OnCommandRemoved(Command* command) {
@@ -309,14 +309,17 @@
     return command;
   }
 
-  bool CanAccessCommand(const std::string& owner_id,
+  bool CanAccessCommand(const UserAppId& owner,
                         const UserInfo& user_info,
                         ErrorPtr* error) const {
     CHECK(user_info.scope() != AuthScope::kNone);
-    CHECK(!user_info.user_id().empty());
+    CHECK(!user_info.id().IsEmpty());
 
     if (user_info.scope() == AuthScope::kManager ||
-        owner_id == user_info.user_id()) {
+        (owner.type == user_info.id().type &&
+         owner.user == user_info.id().user &&
+         (user_info.id().app.empty() ||  // Token is not restricted to the app.
+          owner.app == user_info.id().app))) {
       return true;
     }
 
@@ -341,7 +344,7 @@
   int registation_retry_count_{0};
 
   // Map of command IDs to user IDs.
-  std::map<std::string, std::string> command_owners_;
+  std::map<std::string, UserAppId> command_owners_;
 
   // Backoff entry for retrying device registration.
   BackoffEntry backoff_entry_{&register_backoff_policy};
diff --git a/src/privet/mock_delegates.h b/src/privet/mock_delegates.h
index c75d438..c2e9a89 100644
--- a/src/privet/mock_delegates.h
+++ b/src/privet/mock_delegates.h
@@ -28,6 +28,11 @@
 
 namespace privet {
 
+struct TestUserId : public UserAppId {
+  TestUserId(const std::string& user_id)
+      : UserAppId{AuthType::kAnonymous, {user_id.begin(), user_id.end()}, {}} {}
+};
+
 ACTION_TEMPLATE(RunCallback,
                 HAS_1_TEMPLATE_PARAMS(int, k),
                 AND_0_VALUE_PARAMS()) {
@@ -103,9 +108,12 @@
         .WillRepeatedly(Return(true));
 
     EXPECT_CALL(*this, ParseAccessToken(_, _, _))
-        .WillRepeatedly(
-            DoAll(SetArgPointee<1>(UserInfo{AuthScope::kViewer, "1234567"}),
-                  Return(true)));
+        .WillRepeatedly(DoAll(SetArgPointee<1>(UserInfo{
+                                  AuthScope::kViewer,
+                                  UserAppId{AuthType::kLocal,
+                                            {'1', '2', '3', '4', '5', '6', '7'},
+                                            {}}}),
+                              Return(true)));
 
     EXPECT_CALL(*this, GetPairingTypes())
         .WillRepeatedly(Return(std::set<PairingType>{
diff --git a/src/privet/privet_handler_unittest.cc b/src/privet/privet_handler_unittest.cc
index fa79e77..20f5aa0 100644
--- a/src/privet/privet_handler_unittest.cc
+++ b/src/privet/privet_handler_unittest.cc
@@ -484,7 +484,8 @@
     auth_header_ = "Privet 123";
     EXPECT_CALL(security_, ParseAccessToken(_, _, _))
         .WillRepeatedly(DoAll(
-            SetArgPointee<1>(UserInfo{AuthScope::kOwner, "1"}), Return(true)));
+            SetArgPointee<1>(UserInfo{AuthScope::kOwner, TestUserId{"1"}}),
+            Return(true)));
   }
 };
 
@@ -658,7 +659,8 @@
 TEST_F(PrivetHandlerSetupTest, GcdSetupAsMaster) {
   EXPECT_CALL(security_, ParseAccessToken(_, _, _))
       .WillRepeatedly(DoAll(
-          SetArgPointee<1>(UserInfo{AuthScope::kManager, "1"}), Return(true)));
+          SetArgPointee<1>(UserInfo{AuthScope::kManager, TestUserId{"1"}}),
+          Return(true)));
   const char kInput[] = R"({
     'gcd': {
       'ticketId': 'testTicket',
diff --git a/src/privet/privet_types.h b/src/privet/privet_types.h
index 49c4522..0f51862 100644
--- a/src/privet/privet_types.h
+++ b/src/privet/privet_types.h
@@ -29,17 +29,42 @@
   kWifi50,
 };
 
+struct UserAppId {
+  UserAppId() = default;
+
+  UserAppId(AuthType auth_type,
+            const std::vector<uint8_t>& user_id,
+            const std::vector<uint8_t>& app_id)
+      : type{auth_type},
+        user{user_id},
+        app{user_id.empty() ? user_id : app_id} {}
+
+  bool IsEmpty() const { return user.empty(); }
+
+  AuthType type{};
+  std::vector<uint8_t> user;
+  std::vector<uint8_t> app;
+};
+
+inline bool operator==(const UserAppId& l, const UserAppId& r) {
+  return l.user == r.user && l.app == r.app;
+}
+
+inline bool operator!=(const UserAppId& l, const UserAppId& r) {
+  return l.user != r.user || l.app != r.app;
+}
+
 class UserInfo {
  public:
   explicit UserInfo(AuthScope scope = AuthScope::kNone,
-                    const std::string& user_id = {})
-      : scope_{scope}, user_id_{scope == AuthScope::kNone ? "" : user_id} {}
+                    const UserAppId& id = {})
+      : scope_{scope}, id_{scope == AuthScope::kNone ? UserAppId{} : id} {}
   AuthScope scope() const { return scope_; }
-  const std::string& user_id() const { return user_id_; }
+  const UserAppId& id() const { return id_; }
 
  private:
   AuthScope scope_;
-  std::string user_id_;
+  UserAppId id_;
 };
 
 class ConnectionState final {
diff --git a/src/privet/security_manager.cc b/src/privet/security_manager.cc
index 04164b3..3b08613 100644
--- a/src/privet/security_manager.cc
+++ b/src/privet/security_manager.cc
@@ -91,9 +91,10 @@
                                             std::vector<uint8_t>* access_token,
                                             AuthScope* access_token_scope,
                                             base::TimeDelta* access_token_ttl) {
-  UserInfo user_info{desired_scope,
-                     std::to_string(static_cast<int>(auth_type)) + "/" +
-                         std::to_string(++last_user_id_)};
+  auto user_id = std::to_string(++last_user_id_);
+  UserInfo user_info{
+      desired_scope,
+      UserAppId{auth_type, {user_id.begin(), user_id.end()}, {}}};
 
   const base::TimeDelta kTtl =
       base::TimeDelta::FromSeconds(kAccessTokenExpirationSeconds);
diff --git a/src/privet/security_manager_unittest.cc b/src/privet/security_manager_unittest.cc
index 43b7f00..f596de9 100644
--- a/src/privet/security_manager_unittest.cc
+++ b/src/privet/security_manager_unittest.cc
@@ -25,6 +25,7 @@
 #include "src/config.h"
 #include "src/data_encoding.h"
 #include "src/privet/auth_manager.h"
+#include "src/privet/mock_delegates.h"
 #include "src/privet/openssl_utils.h"
 #include "src/test/mock_clock.h"
 #include "third_party/chromium/crypto/p224_spake.h"
@@ -170,7 +171,7 @@
     UserInfo info;
     EXPECT_TRUE(security_.ParseAccessToken(token, &info, nullptr));
     EXPECT_EQ(requested_scope, info.scope());
-    EXPECT_EQ("0/" + std::to_string(i), info.user_id());
+    EXPECT_EQ(TestUserId{std::to_string(i)}, info.id());
   }
 }