Skip to content

Add token source API for fetching LiveKit tokens#177

Draft
alan-george-lk wants to merge 26 commits into
mainfrom
feature/token_source_api
Draft

Add token source API for fetching LiveKit tokens#177
alan-george-lk wants to merge 26 commits into
mainfrom
feature/token_source_api

Conversation

@alan-george-lk

@alan-george-lk alan-george-lk commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds new public API token_source.h, allowing users to now mint tokens using the following paths:
    • Literal: user-provided token (basically the legacy livekit-cli minted path)
    • Sandbox: enable a sandbox token server via LiveKit cloud, and allow users insecurely mint tokens (development only)
    • Endpoint: production-level server support
  • Additional public API changes include:
    • room.h -> new ways to connect with tokens
    • room_event_types.h -> new information-only event type when tokens are refreshed by the server
    • room_delegate.h -> new callback with the new event

New Dependencies

  • libcurl4-openssl - used for HTTP token endpoint support
  • nlohmann/json - JSON serialization/deserialization support for all configurable token source types

Testing

Validated through a combination of unit, integration, and standalone binary tester program against a LiveKit Cloud agent with a live token server.

Sandbox Token

Connected to a LiveKit cloud instance. The Participant Name field is randomized on program re-run which makes sense.

The token is obtained via the following new API:

auto sandbox_token = livekit::SandboxTokenSource::fromSandboxId("<project ID>");

And results printed below:

alan.george@Alans-MacBook-Pro client-sdk-cpp % ./build-release/bin/token_source_tester
Server URL: <omitted but valid>
Participant Token: <omitted but valid>
Participant Name: efficient-sensor
Connected to room
Local participant: cpp-test-a

@xianshijing-lk xianshijing-lk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might need help to understand the TokenSource a bit more

Comment thread include/livekit/room.h Outdated
@alan-george-lk alan-george-lk force-pushed the feature/token_source_api branch 2 times, most recently from 9e60f34 to 1929f4a Compare June 22, 2026 16:44
Comment thread .github/workflows/make-release.yml Fixed
Comment thread .github/workflows/make-release.yml Fixed
@alan-george-lk alan-george-lk force-pushed the feature/token_source_api branch from 763d27e to 90d262f Compare June 22, 2026 19:52
@MaxHeimbrock

Copy link
Copy Markdown

I am using the tokensource sandbox and it is working fine for me 👍

@xianshijing-lk xianshijing-lk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm with some comments, please address them.

Great work!!!

Comment thread docs/token-lifecycle.md Outdated
@@ -0,0 +1,109 @@
# Token lifecycle

Succinct reference for how join credentials and in-session refresh interact in

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1egoman , could you please help review this token-lifecycle.md ?

@alan-george-lk alan-george-lk Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually generated this for him while I had some active LLM context around this topic, I removed it as I don't want this SDK to be an authoritative documentation source compared to docs.livekit.io.

I do think we should have some better docs around token source and lifetimes, while going through this process it felt a little disjointed and agents/frontend-focused and not just a general document for token source terms, functions, lifecycles, etc for client SDKs.

Comment thread include/livekit/room.h Outdated
#include "livekit/room_event_types.h"
#include "livekit/stats.h"
#include "livekit/subscription_thread_dispatcher.h"
#include "livekit/token_source.h"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, would you consider forward declare those types, and move the #include "livekit/token_source.h" to the cpp ?

One benefit for it is that it could speed up the build.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Comment thread include/livekit/token_source.h Outdated
/// @brief Base interface for token sources that provide full credentials directly.
class LIVEKIT_API TokenSourceFixed {
public:
virtual ~TokenSourceFixed();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, just do "virtual ~TokenSourceFixed() = default" here and remove the cpp line

The same for virtual ~TokenSourceConfigurable() = default;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


/// @brief Error returned when token fetching fails.
struct TokenSourceError {
std::string message;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, do we also want the http status_code ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable idea, but I would advocate not doing this. This error applies to all token source types (including literal and custom, which have no external comms), so adding specific details to any one implementation wouldn't be clean in my view.

If we get a user request for HTTP details for some reason, we could extend or more generalize error specifics into this struct.

Comment thread src/room.cpp Outdated
return connect(details.value().server_url, details.value().participant_token, options);
}

bool Room::connect(TokenSourceConfigurable& token_source, const TokenRequestOptions& request_options,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider a helper function like

namespace {

template <typename FetchFn>
bool connectWithTokenSource(Room& room, const RoomOptions& options, FetchFn&& fetch) {
  Result<TokenSourceResponse, TokenSourceError> details =
      Result<TokenSourceResponse, TokenSourceError>::failure(TokenSourceError{"token source not invoked"});

  try {
    details = fetch().get();
  } catch (const std::exception& e) {
    LK_LOG_ERROR("Room::connect failed: token source threw: {}", e.what());
    return false;
  } catch (...) {
    LK_LOG_ERROR("Room::connect failed: token source threw unknown exception");
    return false;
  }

  if (!details) {
    LK_LOG_ERROR("Room::connect failed: token source error: {}", details.error().message);
    return false;
  }

  const auto& value = details.value();
  return room.connect(value.server_url, value.participant_token, options);
}

} // namespace

Then these two function can become:

bool Room::connect(TokenSourceFixed& token_source, const RoomOptions& options) {
  return connectWithTokenSource(*this, options, [&] {
    return token_source.fetch();
  });
}

bool Room::connect(TokenSourceConfigurable& token_source,
                   const TokenRequestOptions& request_options,
                   const RoomOptions& options) {
  return connectWithTokenSource(*this, options, [&] {
    return token_source.fetch(request_options, false);
  });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, we opted not to do this in other sdks, and determined this was the session api's job. Relevant pull request: livekit/client-sdk-js#1677

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

Comment thread src/token_source.cpp Outdated
: provider_(std::move(provider)) {}

std::future<Result<TokenSourceResponse, TokenSourceError>> CustomTokenSource::fetch(const TokenRequestOptions& options,
bool /*force_refresh*/) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea why CustomTokenSource will skip the force_refresh ?

@alan-george-lk alan-george-lk Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment actually led to an API design change, see here: e39e55e

Having such an argument for a custom source would require passing that argument along to the custom implementation, which felt weird as the custom token source was just a user function that could decide anything it needed to. I have instead dropped force_refresh entirely, and opted for caching_token_source.invalidate() which matches how Swift/Android do it.

In short, this original design around forcing refresh was modeled after JS with the force_refresh. But JS actually goes for an inheritance-based approach to cacheing token sources, vs. here which is using a decorator (Swift/Android do it this way), so forcing refresh was consistent across all sub-classes and didn't have an awkward drop like this did.

It was basically a divergence in both approaches (force + decorator vs. force + inheritance), now it's more aligned to Swift/Android.

Comment thread src/token_source.cpp Outdated
return TokenSourceResult::success(*cached_details_);
}

auto result = inner_->fetch(*options_snapshot, force_refresh).get();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why we want to run the inner_->fetch() under the lock ?

Will it be safer to do ?

{
  std::scoped_lock lock(mutex_);
  if (!force_refresh && cached_details_ && cached_options_ &&
      tokenRequestOptionsEqual(*cached_options_, *options_snapshot) &&
      isParticipantTokenValid(cached_details_->participant_token)) {
    return TokenSourceResult::success(*cached_details_);
  }
}

auto result = inner_->fetch(*options_snapshot, force_refresh).get();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding the mutex over the fetch is how JS and Swift do it, I'm open to your change but it would be a divergence

Comment thread src/token_source.cpp
auto source = std::unique_ptr<SandboxTokenSource>(new SandboxTokenSource(sandbox_id, options, base_url));
auto resolved = resolveSandboxEndpoint(sandbox_id, std::move(options), base_url);
source->endpoint_ =
EndpointTokenSourceTestAccess::create(std::move(resolved.url), std::move(resolved.options), std::move(transport));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the endpoint_ created in line 210 already ? any reason why it needs to be re-created ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, this slipped through. Now have a private constructor that accepts an already made endpoint so both the public and test access versions can avoid recreation

Comment thread src/token_source_http.cpp Outdated
}

const std::wstring host(components.lpszHostName, components.dwHostNameLength);
const std::wstring path(components.lpszUrlPath, components.dwUrlPathLength);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question, does path need to include lpszExtraInfo ? like when lpszExtraInfoLength > 0 ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

Comment thread src/token_source_http.cpp
}

const int timeout_ms = static_cast<int>(timeout.count());
WinHttpSetTimeouts(session, timeout_ms, timeout_ms, timeout_ms, timeout_ms);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will happen if timeout_ms is 0 or not valid ?

@alan-george-lk alan-george-lk Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, <= 0 now goes to the default of 30 seconds

@alan-george-lk alan-george-lk requested a review from 1egoman June 25, 2026 17:57

@1egoman 1egoman left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good to me, and I think matches the interfaces on other sdks pretty well!

Comment on lines +114 to +118
/// LiveKit Cloud deployment to target for agent dispatch.
///
/// Optional. When omitted or empty, the production deployment is used.
/// Only relevant when dispatching a named agent on LiveKit Cloud.
std::optional<std::string> agent_deployment;

@1egoman 1egoman Jun 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: in other sdks (for example, web) this was just called deployment. I like this agentDeployment name better though and had brought it up during the review process when it was added recently but I think it got missed. It might be worth changing this for consistency, idk.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android and Swift (which this largely models) both use agentDeployment. So I think it's a 50/50 consistency roll of the dice 🤔

Comment on lines +179 to +182
/// @brief Create a token source from an async provider that returns full credentials.
///
/// Use this overload when credentials are produced outside the SDK but fetched
/// lazily (for example, from your own cache or secure storage).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: It might be worth re-emphasizing in the docs here (assuming this works the same as the web and other implementations) that this doesn't support parameters and that if you want a configurable version of this which can re-fetch multiple times on demand to use CustomTokenSource

Comment thread src/token_source.cpp
Comment on lines +32 to +37
bool tokenRequestOptionsEqual(const TokenRequestOptions& a, const TokenRequestOptions& b) {
return a.room_name == b.room_name && a.participant_name == b.participant_name &&
a.participant_identity == b.participant_identity && a.participant_metadata == b.participant_metadata &&
a.participant_attributes == b.participant_attributes && a.agent_name == b.agent_name &&
a.agent_metadata == b.agent_metadata && a.agent_deployment == b.agent_deployment;
}

@1egoman 1egoman Jun 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I'm not sure if there's a way to handle this in c++, but ideally if possible it would be great if the compiler could somehow throw an error if a field in a or b were missed in this check.

Also, do you have to worry about deep equality here within maps like participant_attributes? Or is == on the fields directly good enough?

Comment on lines +286 to +291
/// @brief Decorator that adds JWT-aware caching to another configurable token source.
///
/// Wrap @ref CustomTokenSource, @ref EndpointTokenSource, or
/// @ref SandboxTokenSource to reduce token fetch calls while still refreshing
/// when tokens expire or when @p force_refresh is requested.
class LIVEKIT_API CachingTokenSource final : public TokenSourceConfigurable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: You probably have seen it / maybe even patterned it off of this, but your implementation here is fairly close to the swift version: https://github.com/livekit/client-sdk-swift/blob/5d13e9ca7b366a8b47bd08c95a34cb0068149287/Sources/LiveKit/Token/CachingTokenSource.swift#L23.

Also just want to confirm - looking at the cpp I think this is just memoization with the last value correct? There's no other way to configure this right? I think that fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants