Add token expiration time checks to CachedBlindSignAuth to ensure that it only returns fresh tokens. Token expiration time is based on the AT server's signing public key.

PiperOrigin-RevId: 532797040
diff --git a/quiche/blind_sign_auth/blind_sign_auth.cc b/quiche/blind_sign_auth/blind_sign_auth.cc
index 8715eac..b1d8ae5 100644
--- a/quiche/blind_sign_auth/blind_sign_auth.cc
+++ b/quiche/blind_sign_auth/blind_sign_auth.cc
@@ -7,6 +7,7 @@
 #include <cstddef>
 #include <functional>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "quiche/blind_sign_auth/proto/auth_and_sign.pb.h"
@@ -38,8 +39,7 @@
 
 void BlindSignAuth::GetTokens(
     absl::string_view oauth_token, int num_tokens,
-    std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-        callback) {
+    std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback) {
   // Create GetInitialData RPC.
   privacy::ppn::GetInitialDataRequest request;
   request.set_use_attestation(false);
@@ -61,7 +61,7 @@
 void BlindSignAuth::GetInitialDataCallback(
     absl::StatusOr<BlindSignHttpResponse> response,
     absl::string_view oauth_token, int num_tokens,
-    std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback) {
+    std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback) {
   if (!response.ok()) {
     QUICHE_LOG(WARNING) << "GetInitialDataRequest failed: "
                         << response.status();
@@ -93,6 +93,14 @@
     callback(bssa_client.status());
     return;
   }
+  absl::StatusOr<absl::Time> public_key_expiry_time =
+      private_membership::anonymous_tokens::TimeFromProto(
+          initial_data_response.at_public_metadata_public_key()
+              .expiration_time());
+  if (!public_key_expiry_time.ok()) {
+    callback(absl::InternalError("Failed to parse public key expiration time"));
+    return;
+  }
 
   // Create plaintext tokens.
   // Client blinds plaintext tokens (random 32-byte strings) in CreateRequest.
@@ -155,21 +163,23 @@
       "/v1/authWithHeaderCreds", oauth_token.data(),
       sign_request.SerializeAsString(),
       [this, at_sign_request, public_metadata_info,
+       expiry_time_ = public_key_expiry_time.value(),
        bssa_client_ = bssa_client.value().get(),
        callback](absl::StatusOr<BlindSignHttpResponse> response) {
-        AuthAndSignCallback(response, public_metadata_info, *at_sign_request,
-                            bssa_client_, callback);
+        AuthAndSignCallback(response, public_metadata_info, expiry_time_,
+                            *at_sign_request, bssa_client_, callback);
       });
 }
 
 void BlindSignAuth::AuthAndSignCallback(
     absl::StatusOr<BlindSignHttpResponse> response,
     privacy::ppn::PublicMetadataInfo public_metadata_info,
+    absl::Time public_key_expiry_time,
     private_membership::anonymous_tokens::AnonymousTokensSignRequest
         at_sign_request,
     private_membership::anonymous_tokens::AnonymousTokensRsaBssaClient*
         bssa_client,
-    std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback) {
+    std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback) {
   // Validate response.
   if (!response.ok()) {
     QUICHE_LOG(WARNING) << "AuthAndSign failed: " << response.status();
@@ -245,7 +255,7 @@
   }
 
   // Output SpendTokenData with data for the redeemer to make a SpendToken RPC.
-  std::vector<std::string> tokens_vec;
+  std::vector<BlindSignToken> tokens_vec;
   for (size_t i = 0; i < signed_tokens->size(); i++) {
     privacy::ppn::SpendTokenData spend_token_data;
     *spend_token_data.mutable_public_metadata() =
@@ -266,10 +276,11 @@
     spend_token_data.set_use_case(*use_case);
     spend_token_data.set_message_mask(
         signed_tokens->at(i).token().message_mask());
-    tokens_vec.push_back(spend_token_data.SerializeAsString());
+    tokens_vec.push_back(BlindSignToken{spend_token_data.SerializeAsString(),
+                                        public_key_expiry_time});
   }
 
-  callback(absl::Span<std::string>(tokens_vec));
+  callback(absl::Span<BlindSignToken>(tokens_vec));
 }
 
 absl::Status BlindSignAuth::FingerprintPublicMetadata(
diff --git a/quiche/blind_sign_auth/blind_sign_auth.h b/quiche/blind_sign_auth/blind_sign_auth.h
index 5a9384b..710659f 100644
--- a/quiche/blind_sign_auth/blind_sign_auth.h
+++ b/quiche/blind_sign_auth/blind_sign_auth.h
@@ -8,11 +8,13 @@
 #include <functional>
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "quiche/blind_sign_auth/proto/public_metadata.pb.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
+#include "absl/time/time.h"
 #include "absl/types/span.h"
 #include "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h"
 #include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h"
@@ -29,30 +31,31 @@
   explicit BlindSignAuth(BlindSignHttpInterface* http_fetcher)
       : http_fetcher_(http_fetcher) {}
 
-  // Returns signed unblinded tokens in a callback. Tokens are single-use.
+  // Returns signed unblinded tokens and their expiration time in a callback.
+  // Tokens are single-use.
   // GetTokens starts asynchronous HTTP POST requests to a signer hostname
   // specified by the caller, with path and query params given in the request.
   // The GetTokens callback will run on the same thread as the
   // BlindSignHttpInterface callbacks.
   // Callers can make multiple concurrent requests to GetTokens.
-  void GetTokens(
-      absl::string_view oauth_token, int num_tokens,
-      std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-          callback) override;
+  void GetTokens(absl::string_view oauth_token, int num_tokens,
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+                     callback) override;
 
  private:
   void GetInitialDataCallback(
       absl::StatusOr<BlindSignHttpResponse> response,
       absl::string_view oauth_token, int num_tokens,
-      std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback);
+      std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback);
   void AuthAndSignCallback(
       absl::StatusOr<BlindSignHttpResponse> response,
       privacy::ppn::PublicMetadataInfo public_metadata_info,
+      absl::Time public_key_expiry_time,
       private_membership::anonymous_tokens::AnonymousTokensSignRequest
           at_sign_request,
       private_membership::anonymous_tokens::AnonymousTokensRsaBssaClient*
           bssa_client,
-      std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback);
+      std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback);
   absl::Status FingerprintPublicMetadata(
       const privacy::ppn::PublicMetadata& metadata, uint64_t* fingerprint);
 
diff --git a/quiche/blind_sign_auth/blind_sign_auth_interface.h b/quiche/blind_sign_auth/blind_sign_auth_interface.h
index f7e3905..ac5b10a 100644
--- a/quiche/blind_sign_auth/blind_sign_auth_interface.h
+++ b/quiche/blind_sign_auth/blind_sign_auth_interface.h
@@ -10,11 +10,20 @@
 
 #include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
+#include "absl/time/time.h"
 #include "absl/types/span.h"
 #include "quiche/common/platform/api/quiche_export.h"
 
 namespace quiche {
 
+// A BlindSignToken is used to authenticate a request to a privacy proxy.
+// The token string contains a serialized SpendTokenData proto.
+// The token cannot be successfully redeemed after the expiration time.
+struct QUICHE_EXPORT BlindSignToken {
+  std::string token;
+  absl::Time expiration;
+};
+
 // BlindSignAuth provides signed, unblinded tokens to callers.
 class QUICHE_EXPORT BlindSignAuthInterface {
  public:
@@ -23,7 +32,7 @@
   // Returns signed unblinded tokens in a callback. Tokens are single-use.
   virtual void GetTokens(
       absl::string_view oauth_token, int num_tokens,
-      std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
+      std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
           callback) = 0;
 };
 
diff --git a/quiche/blind_sign_auth/blind_sign_auth_test.cc b/quiche/blind_sign_auth/blind_sign_auth_test.cc
index bb9d8da..3a5d842 100644
--- a/quiche/blind_sign_auth/blind_sign_auth_test.cc
+++ b/quiche/blind_sign_auth/blind_sign_auth_test.cc
@@ -129,10 +129,10 @@
     sign_response_ = response;
   }
 
-  void ValidateGetTokensOutput(const absl::Span<const std::string>& tokens) {
+  void ValidateGetTokensOutput(const absl::Span<BlindSignToken>& tokens) {
     for (const auto& token : tokens) {
       privacy::ppn::SpendTokenData spend_token_data;
-      ASSERT_TRUE(spend_token_data.ParseFromString(token));
+      ASSERT_TRUE(spend_token_data.ParseFromString(token.token));
       // Validate token structure.
       EXPECT_EQ(spend_token_data.public_metadata().SerializeAsString(),
                 public_metadata_info_.public_metadata().SerializeAsString());
@@ -191,9 +191,9 @@
 
   int num_tokens = 1;
   QuicheNotification done;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
       [this, &done,
-       num_tokens](absl::StatusOr<absl::Span<const std::string>> tokens) {
+       num_tokens](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         QUICHE_EXPECT_OK(tokens);
         EXPECT_EQ(tokens->size(), num_tokens);
         ValidateGetTokensOutput(*tokens);
@@ -216,8 +216,8 @@
 
   int num_tokens = 1;
   QuicheNotification done;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [&done](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal);
         done.Notify();
       };
@@ -245,8 +245,8 @@
 
   int num_tokens = 1;
   QuicheNotification done;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [&done](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
         done.Notify();
       };
@@ -286,8 +286,8 @@
 
   int num_tokens = 1;
   QuicheNotification done;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [&done](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal);
         done.Notify();
       };
diff --git a/quiche/blind_sign_auth/cached_blind_sign_auth.cc b/quiche/blind_sign_auth/cached_blind_sign_auth.cc
index 34e5e73..7c9271d 100644
--- a/quiche/blind_sign_auth/cached_blind_sign_auth.cc
+++ b/quiche/blind_sign_auth/cached_blind_sign_auth.cc
@@ -8,17 +8,22 @@
 #include <vector>
 
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/str_format.h"
+#include "absl/time/clock.h"
+#include "absl/time/time.h"
 #include "absl/types/span.h"
+#include "quiche/blind_sign_auth/blind_sign_auth_interface.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/platform/api/quiche_mutex.h"
 
 namespace quiche {
 
+constexpr absl::Duration kFreshnessConstant = absl::Minutes(5);
+
 void CachedBlindSignAuth::GetTokens(
     absl::string_view oauth_token, int num_tokens,
-    std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-        callback) {
+    std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback) {
   if (num_tokens > max_tokens_per_request_) {
     callback(absl::InvalidArgumentError(
         absl::StrFormat("Number of tokens requested exceeds maximum: %d",
@@ -31,25 +36,27 @@
     return;
   }
 
-  std::vector<std::string> output_tokens;
+  std::vector<BlindSignToken> output_tokens;
   {
     QuicheWriterMutexLock lock(&mutex_);
 
+    RemoveExpiredTokens();
     // Try to fill the request from cache.
     if (static_cast<size_t>(num_tokens) <= cached_tokens_.size()) {
       output_tokens = CreateOutputTokens(num_tokens);
     }
   }
+
   if (!output_tokens.empty() || num_tokens == 0) {
-    callback(output_tokens);
+    callback(absl::MakeSpan(output_tokens));
     return;
   }
 
   // Make a GetTokensRequest if the cache can't handle the request size.
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
       caching_callback =
           [this, num_tokens,
-           callback](absl::StatusOr<absl::Span<const std::string>> tokens) {
+           callback](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
             HandleGetTokensResponse(tokens, num_tokens, callback);
           };
   blind_sign_auth_->GetTokens(oauth_token, kBlindSignAuthRequestMaxTokens,
@@ -57,9 +64,8 @@
 }
 
 void CachedBlindSignAuth::HandleGetTokensResponse(
-    absl::StatusOr<absl::Span<const std::string>> tokens, int num_tokens,
-    std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-        callback) {
+    absl::StatusOr<absl::Span<BlindSignToken>> tokens, int num_tokens,
+    std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback) {
   if (!tokens.ok()) {
     QUICHE_LOG(WARNING) << "BlindSignAuth::GetTokens failed: "
                         << tokens.status();
@@ -72,16 +78,16 @@
                         << tokens->size();
   }
 
-  std::vector<std::string> output_tokens;
+  std::vector<BlindSignToken> output_tokens;
   size_t cache_size;
   {
     QuicheWriterMutexLock lock(&mutex_);
 
     // Add returned tokens to cache.
-    for (const std::string& token : *tokens) {
+    for (const BlindSignToken& token : *tokens) {
       cached_tokens_.push_back(token);
     }
-
+    RemoveExpiredTokens();
     // Return tokens or a ResourceExhaustedError.
     cache_size = cached_tokens_.size();
     if (cache_size >= static_cast<size_t>(num_tokens)) {
@@ -90,7 +96,7 @@
   }
 
   if (!output_tokens.empty()) {
-    callback(output_tokens);
+    callback(absl::MakeSpan(output_tokens));
     return;
   }
   callback(absl::ResourceExhaustedError(absl::StrFormat(
@@ -98,9 +104,9 @@
       num_tokens, cache_size)));
 }
 
-std::vector<std::string> CachedBlindSignAuth::CreateOutputTokens(
+std::vector<BlindSignToken> CachedBlindSignAuth::CreateOutputTokens(
     int num_tokens) {
-  std::vector<std::string> output_tokens;
+  std::vector<BlindSignToken> output_tokens;
   if (cached_tokens_.size() < static_cast<size_t>(num_tokens)) {
     QUICHE_LOG(FATAL) << "Check failed, not enough tokens in cache: "
                       << cached_tokens_.size() << " < " << num_tokens;
@@ -112,4 +118,16 @@
   return output_tokens;
 }
 
+void CachedBlindSignAuth::RemoveExpiredTokens() {
+  size_t original_size = cached_tokens_.size();
+  absl::Time now_plus_five_mins = absl::Now() + kFreshnessConstant;
+  for (size_t i = 0; i < original_size; i++) {
+    BlindSignToken token = std::move(cached_tokens_.front());
+    cached_tokens_.pop_front();
+    if (token.expiration > now_plus_five_mins) {
+      cached_tokens_.push_back(std::move(token));
+    }
+  }
+}
+
 }  // namespace quiche
diff --git a/quiche/blind_sign_auth/cached_blind_sign_auth.h b/quiche/blind_sign_auth/cached_blind_sign_auth.h
index ee405a1..f976fb1 100644
--- a/quiche/blind_sign_auth/cached_blind_sign_auth.h
+++ b/quiche/blind_sign_auth/cached_blind_sign_auth.h
@@ -35,29 +35,29 @@
       : blind_sign_auth_(blind_sign_auth),
         max_tokens_per_request_(max_tokens_per_request) {}
 
-  // Returns signed unblinded tokens in a callback. Tokens are single-use.
+  // Returns signed unblinded tokens and expiration time in a callback.
+  // Tokens are single-use. They will not be usable after the expiration time.
   //
   // The GetTokens callback may be called synchronously on the calling thread,
   // or asynchronously on BlindSignAuth's BlindSignHttpInterface thread.
   // The GetTokens callback must not acquire any locks that the calling thread
   // owns, otherwise the callback will deadlock.
-  void GetTokens(
-      absl::string_view oauth_token, int num_tokens,
-      std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-          callback) override;
+  void GetTokens(absl::string_view oauth_token, int num_tokens,
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+                     callback) override;
 
  private:
   void HandleGetTokensResponse(
-      absl::StatusOr<absl::Span<const std::string>> tokens, int num_tokens,
-      std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-          callback);
-  std::vector<std::string> CreateOutputTokens(int num_tokens)
+      absl::StatusOr<absl::Span<BlindSignToken>> tokens, int num_tokens,
+      std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback);
+  std::vector<BlindSignToken> CreateOutputTokens(int num_tokens)
       QUICHE_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  void RemoveExpiredTokens() QUICHE_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   BlindSignAuthInterface* blind_sign_auth_;
   int max_tokens_per_request_;
   QuicheMutex mutex_;
-  QuicheCircularDeque<std::string> cached_tokens_ QUICHE_GUARDED_BY(mutex_);
+  QuicheCircularDeque<BlindSignToken> cached_tokens_ QUICHE_GUARDED_BY(mutex_);
 };
 
 }  // namespace quiche
diff --git a/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc b/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc
index dfad523..9bc7ee3 100644
--- a/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc
+++ b/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc
@@ -7,12 +7,15 @@
 #include <functional>
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_format.h"
+#include "absl/time/clock.h"
+#include "absl/time/time.h"
 #include "absl/types/span.h"
 #include "quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h"
 #include "quiche/common/platform/api/quiche_mutex.h"
@@ -41,17 +44,28 @@
   }
 
  public:
-  std::vector<std::string> MakeFakeTokens(int num_tokens) {
-    std::vector<std::string> fake_tokens;
+  std::vector<BlindSignToken> MakeFakeTokens(int num_tokens) {
+    std::vector<BlindSignToken> fake_tokens;
     for (int i = 0; i < kBlindSignAuthRequestMaxTokens; i++) {
-      fake_tokens.push_back(absl::StrCat("token:", i));
+      fake_tokens.push_back(BlindSignToken{absl::StrCat("token:", i),
+                                           absl::Now() + absl::Hours(1)});
     }
     return fake_tokens;
   }
+
+  std::vector<BlindSignToken> MakeExpiredTokens(int num_tokens) {
+    std::vector<BlindSignToken> fake_tokens;
+    for (int i = 0; i < kBlindSignAuthRequestMaxTokens; i++) {
+      fake_tokens.push_back(BlindSignToken{absl::StrCat("token:", i),
+                                           absl::Now() - absl::Hours(1)});
+    }
+    return fake_tokens;
+  }
+
   MockBlindSignAuthInterface mock_blind_sign_auth_interface_;
   std::unique_ptr<CachedBlindSignAuth> cached_blind_sign_auth_;
   std::string oauth_token_ = "oauth_token";
-  std::vector<std::string> fake_tokens_;
+  std::vector<BlindSignToken> fake_tokens_;
 };
 
 TEST_F(CachedBlindSignAuthTest, TestGetTokensOneCallSuccessful) {
@@ -60,7 +74,7 @@
       .Times(1)
       .WillOnce(Invoke(
           [this](Unused, int num_tokens,
-                 std::function<void(absl::StatusOr<absl::Span<std::string>>)>
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
                      callback) {
             fake_tokens_ = MakeFakeTokens(num_tokens);
             callback(absl::MakeSpan(fake_tokens_));
@@ -68,13 +82,12 @@
 
   int num_tokens = 5;
   QuicheNotification done;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [num_tokens,
-       &done](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [num_tokens, &done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         QUICHE_EXPECT_OK(tokens);
         EXPECT_EQ(num_tokens, tokens->size());
         for (int i = 0; i < num_tokens; i++) {
-          EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i));
+          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
         }
         done.Notify();
       };
@@ -89,7 +102,7 @@
       .Times(2)
       .WillRepeatedly(Invoke(
           [this](Unused, int num_tokens,
-                 std::function<void(absl::StatusOr<absl::Span<std::string>>)>
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
                      callback) {
             fake_tokens_ = MakeFakeTokens(num_tokens);
             callback(absl::MakeSpan(fake_tokens_));
@@ -97,36 +110,33 @@
 
   int num_tokens = kBlindSignAuthRequestMaxTokens - 1;
   QuicheNotification first;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      first_callback =
-          [num_tokens,
-           &first](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(num_tokens, tokens->size());
-            for (int i = 0; i < num_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i));
-            }
-            first.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      first_callback = [num_tokens, &first](
+                           absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(num_tokens, tokens->size());
+        for (int i = 0; i < num_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
+        }
+        first.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback);
   first.WaitForNotification();
 
   QuicheNotification second;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      second_callback =
-          [num_tokens,
-           &second](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(num_tokens, tokens->size());
-            EXPECT_EQ(
-                tokens->at(0),
-                absl::StrCat("token:", kBlindSignAuthRequestMaxTokens - 1));
-            for (int i = 1; i < num_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i - 1));
-            }
-            second.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      second_callback = [num_tokens, &second](
+                            absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(num_tokens, tokens->size());
+        EXPECT_EQ(tokens->at(0).token,
+                  absl::StrCat("token:", kBlindSignAuthRequestMaxTokens - 1));
+        for (int i = 1; i < num_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i - 1));
+        }
+        second.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback);
   second.WaitForNotification();
@@ -138,7 +148,7 @@
       .Times(1)
       .WillOnce(Invoke(
           [this](Unused, int num_tokens,
-                 std::function<void(absl::StatusOr<absl::Span<std::string>>)>
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
                      callback) {
             fake_tokens_ = MakeFakeTokens(num_tokens);
             callback(absl::MakeSpan(fake_tokens_));
@@ -146,33 +156,32 @@
 
   int num_tokens = kBlindSignAuthRequestMaxTokens / 2;
   QuicheNotification first;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      first_callback =
-          [num_tokens,
-           &first](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(num_tokens, tokens->size());
-            for (int i = 0; i < num_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i));
-            }
-            first.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      first_callback = [num_tokens, &first](
+                           absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(num_tokens, tokens->size());
+        for (int i = 0; i < num_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
+        }
+        first.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback);
   first.WaitForNotification();
 
   QuicheNotification second;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      second_callback =
-          [num_tokens,
-           &second](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(num_tokens, tokens->size());
-            for (int i = 0; i < num_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i + num_tokens));
-            }
-            second.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      second_callback = [num_tokens, &second](
+                            absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(num_tokens, tokens->size());
+        for (int i = 0; i < num_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token,
+                    absl::StrCat("token:", i + num_tokens));
+        }
+        second.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback);
   second.WaitForNotification();
@@ -184,7 +193,7 @@
       .Times(2)
       .WillRepeatedly(Invoke(
           [this](Unused, int num_tokens,
-                 std::function<void(absl::StatusOr<absl::Span<std::string>>)>
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
                      callback) {
             fake_tokens_ = MakeFakeTokens(num_tokens);
             callback(absl::MakeSpan(fake_tokens_));
@@ -192,50 +201,48 @@
 
   int num_tokens = kBlindSignAuthRequestMaxTokens / 2;
   QuicheNotification first;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      first_callback =
-          [num_tokens,
-           &first](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(num_tokens, tokens->size());
-            for (int i = 0; i < num_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i));
-            }
-            first.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      first_callback = [num_tokens, &first](
+                           absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(num_tokens, tokens->size());
+        for (int i = 0; i < num_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
+        }
+        first.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback);
   first.WaitForNotification();
 
   QuicheNotification second;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      second_callback =
-          [num_tokens,
-           &second](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(num_tokens, tokens->size());
-            for (int i = 0; i < num_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i + num_tokens));
-            }
-            second.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      second_callback = [num_tokens, &second](
+                            absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(num_tokens, tokens->size());
+        for (int i = 0; i < num_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token,
+                    absl::StrCat("token:", i + num_tokens));
+        }
+        second.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback);
   second.WaitForNotification();
 
   QuicheNotification third;
   int third_request_tokens = 10;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-      third_callback =
-          [third_request_tokens,
-           &third](absl::StatusOr<absl::Span<const std::string>> tokens) {
-            QUICHE_EXPECT_OK(tokens);
-            EXPECT_EQ(third_request_tokens, tokens->size());
-            for (int i = 0; i < third_request_tokens; i++) {
-              EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i));
-            }
-            third.Notify();
-          };
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      third_callback = [third_request_tokens, &third](
+                           absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+        QUICHE_EXPECT_OK(tokens);
+        EXPECT_EQ(third_request_tokens, tokens->size());
+        for (int i = 0; i < third_request_tokens; i++) {
+          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
+        }
+        third.Notify();
+      };
 
   cached_blind_sign_auth_->GetTokens(oauth_token_, third_request_tokens,
                                      third_callback);
@@ -248,8 +255,8 @@
       .Times(0);
 
   int num_tokens = kBlindSignAuthRequestMaxTokens + 1;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
         EXPECT_THAT(
             tokens.status().message(),
@@ -266,8 +273,8 @@
       .Times(0);
 
   int num_tokens = -1;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [num_tokens](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [num_tokens](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
         EXPECT_THAT(tokens.status().message(),
                     absl::StrFormat("Negative number of tokens requested: %d",
@@ -284,7 +291,7 @@
       .WillOnce(InvokeArgument<2>(absl::InternalError("AuthAndSign failed")))
       .WillOnce(Invoke(
           [this](Unused, int num_tokens,
-                 std::function<void(absl::StatusOr<absl::Span<std::string>>)>
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
                      callback) {
             fake_tokens_ = MakeFakeTokens(num_tokens);
             fake_tokens_.pop_back();
@@ -293,9 +300,9 @@
 
   int num_tokens = kBlindSignAuthRequestMaxTokens;
   QuicheNotification first;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
       first_callback =
-          [&first](absl::StatusOr<absl::Span<const std::string>> tokens) {
+          [&first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
             EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal);
             EXPECT_THAT(tokens.status().message(), "AuthAndSign failed");
             first.Notify();
@@ -305,9 +312,9 @@
   first.WaitForNotification();
 
   QuicheNotification second;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
       second_callback =
-          [&second](absl::StatusOr<absl::Span<const std::string>> tokens) {
+          [&second](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
             EXPECT_THAT(tokens.status().code(),
                         absl::StatusCode::kResourceExhausted);
             second.Notify();
@@ -323,8 +330,8 @@
       .Times(0);
 
   int num_tokens = 0;
-  std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback =
-      [](absl::StatusOr<absl::Span<const std::string>> tokens) {
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> callback =
+      [](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
         QUICHE_EXPECT_OK(tokens);
         EXPECT_EQ(tokens->size(), 0);
       };
@@ -332,6 +339,32 @@
   cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, callback);
 }
 
+TEST_F(CachedBlindSignAuthTest, TestExpiredTokensArePruned) {
+  EXPECT_CALL(mock_blind_sign_auth_interface_,
+              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _))
+      .Times(1)
+      .WillOnce(Invoke(
+          [this](Unused, int num_tokens,
+                 std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+                     callback) {
+            fake_tokens_ = MakeExpiredTokens(num_tokens);
+            callback(absl::MakeSpan(fake_tokens_));
+          }));
+
+  int num_tokens = kBlindSignAuthRequestMaxTokens;
+  QuicheNotification first;
+  std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+      first_callback =
+          [&first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
+            EXPECT_THAT(tokens.status().code(),
+                        absl::StatusCode::kResourceExhausted);
+            first.Notify();
+          };
+
+  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback);
+  first.WaitForNotification();
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quiche
diff --git a/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h b/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h
index dcb4876..378e8bb 100644
--- a/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h
+++ b/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h
@@ -20,12 +20,11 @@
 class QUICHE_NO_EXPORT MockBlindSignAuthInterface
     : public BlindSignAuthInterface {
  public:
-  MOCK_METHOD(
-      void, GetTokens,
-      (absl::string_view oauth_token, int num_tokens,
-       std::function<void(absl::StatusOr<absl::Span<const std::string>>)>
-           callback),
-      (override));
+  MOCK_METHOD(void, GetTokens,
+              (absl::string_view oauth_token, int num_tokens,
+               std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)>
+                   callback),
+              (override));
 };
 
 }  // namespace quiche::test