Skip to content

Add token source API for fetching LiveKit tokens#177

Open
alan-george-lk wants to merge 27 commits into
mainfrom
feature/token_source_api
Open

Add token source API for fetching LiveKit tokens#177
alan-george-lk wants to merge 27 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_delegate.h -> new callback with the new event
    • room_event_types.h -> new information-only event type when tokens are refreshed by the server (see below)

Server Token Refresh

LiveKit server token refresh existed before this feature, but this branch now adds support for the FFI event onTokenRefreshed.

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.

Token Server Action

Integration tests now test against a basic token server implementation run in GitHub actions live (skipped locally).

Sandbox Token

Verified locally by connecting to a LiveKit cloud instance:

auto sandbox_token = livekit::SandboxTokenSource::fromSandboxId("<omitted but valid>");

And results printed below:

./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

Comment thread include/livekit/token_source.h
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
@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 🤔

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ugh I hadn't realized it was so fragmented 😧

Comment thread include/livekit/token_source.h
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?

@alan-george-lk alan-george-lk Jun 26, 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.

For the second part, == is an "operator overload", which is a C++-ism that basically allows you to define actual functions for operators like +, -, *, ==, etc., so for maps it is in fact a deep comparison: https://en.cppreference.com/cpp/container/map/operator_cmp (good callout though!)

For the first one, there are some options, I am looking into that

@alan-george-lk alan-george-lk Jun 26, 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.

@1egoman For the first part, it's a good callout, but until C++20 which now has default bool operators (compiler-generated field quality checks like this does manually), what you see here is basically the way to do it unfortunately. There's some C++17 (what we're on) features to slightly improve the robustness, but it adds a lot of code for little benefit.

My vote would be to leave this as-is as it's unlikely to change often

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.

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.

Yup and yup to your two questions!

@alan-george-lk alan-george-lk marked this pull request as ready for review June 26, 2026 14:32
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