From bf18d8e1069c661edce81736b86f98474ff05bf2 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:36:53 +0100 Subject: [PATCH] Validate that user id matches token --- plugins/twitch/content-classification.cpp | 9 ++- plugins/twitch/language-selection.cpp | 9 ++- plugins/twitch/macro-action-twitch.cpp | 84 ++++++++++++++++++----- plugins/twitch/macro-condition-twitch.cpp | 8 ++- plugins/twitch/tag-selection.cpp | 9 ++- plugins/twitch/token.cpp | 37 ++++++++-- plugins/twitch/token.hpp | 4 +- plugins/twitch/twitch-tab.cpp | 39 ++++++----- 8 files changed, 147 insertions(+), 52 deletions(-) diff --git a/plugins/twitch/content-classification.cpp b/plugins/twitch/content-classification.cpp index 39bcae04..ce509dc0 100644 --- a/plugins/twitch/content-classification.cpp +++ b/plugins/twitch/content-classification.cpp @@ -68,10 +68,15 @@ void ContentClassification::SetContentClassification( data = obs_data_create(); obs_data_set_array(data, "content_classification_labels", ccls); + const auto id = token.GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + auto result = SendPatchRequest(token, "https://api.twitch.tv", "/helix/channels", - {{"broadcaster_id", token.GetUserID()}}, - data.Get()); + {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, diff --git a/plugins/twitch/language-selection.cpp b/plugins/twitch/language-selection.cpp index 4c9de28a..a3164d6a 100644 --- a/plugins/twitch/language-selection.cpp +++ b/plugins/twitch/language-selection.cpp @@ -189,12 +189,17 @@ void LanguageSelection::Save(obs_data_t *obj) const void LanguageSelection::SetStreamLanguage(const TwitchToken &token) const { + const auto id = token.GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "broadcaster_language", _language.c_str()); auto result = SendPatchRequest(token, "https://api.twitch.tv", "/helix/channels", - {{"broadcaster_id", token.GetUserID()}}, - data.Get()); + {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to set stream language! (%d)", diff --git a/plugins/twitch/macro-action-twitch.cpp b/plugins/twitch/macro-action-twitch.cpp index abc95701..590b6d63 100644 --- a/plugins/twitch/macro-action-twitch.cpp +++ b/plugins/twitch/macro-action-twitch.cpp @@ -123,12 +123,17 @@ void MacroActionTwitch::SetStreamTitle( return; } + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "title", _streamTitle.c_str()); auto result = SendPatchRequest(*token, "https://api.twitch.tv", "/helix/channels", - {{"broadcaster_id", token->GetUserID()}}, - data.Get()); + {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to set stream title! (%d)", @@ -143,13 +148,18 @@ void MacroActionTwitch::SetStreamCategory( return; } + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "game_id", std::to_string(_category.id).c_str()); auto result = SendPatchRequest(*token, "https://api.twitch.tv", "/helix/channels", - {{"broadcaster_id", token->GetUserID()}}, - data.Get()); + {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to set stream category! (%d)", @@ -160,8 +170,14 @@ void MacroActionTwitch::SetStreamCategory( void MacroActionTwitch::CreateStreamMarker( const std::shared_ptr &token) const { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "user_id", token->GetUserID().c_str()); + obs_data_set_string(data, "user_id", id->c_str()); if (!std::string(_markerDescription).empty()) { obs_data_set_string(data, "description", @@ -179,12 +195,17 @@ void MacroActionTwitch::CreateStreamMarker( void MacroActionTwitch::CreateStreamClip( const std::shared_ptr &token) const { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + auto hasDelay = _clipHasDelay ? "true" : "false"; - auto result = SendPostRequest(*token, "https://api.twitch.tv", - "/helix/clips", - {{"broadcaster_id", token->GetUserID()}, - {"has_delay", hasDelay}}); + auto result = SendPostRequest( + *token, "https://api.twitch.tv", "/helix/clips", + {{"broadcaster_id", *id}, {"has_delay", hasDelay}}); if (result.status != 202) { blog(LOG_INFO, "Failed to create clip! (%d)", result.status); @@ -194,8 +215,14 @@ void MacroActionTwitch::CreateStreamClip( void MacroActionTwitch::StartCommercial( const std::shared_ptr &token) const { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "broadcaster_id", token->GetUserID().c_str()); + obs_data_set_string(data, "broadcaster_id", id->c_str()); obs_data_set_int(data, "length", _duration.Seconds()); auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/channels/commercial", {}, @@ -227,17 +254,21 @@ void MacroActionTwitch::StartCommercial( void MacroActionTwitch::SendChatAnnouncement( const std::shared_ptr &token) const { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "message", _announcementMessage.c_str()); obs_data_set_string( data, "color", announcementColorsTwitch.at(_announcementColor).c_str()); - auto userId = token->GetUserID(); auto result = SendPostRequest( *token, "https://api.twitch.tv", "/helix/chat/announcements", - {{"broadcaster_id", userId}, {"moderator_id", userId}}, - data.Get()); + {{"broadcaster_id", *id}, {"moderator_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to send chat announcement! (%d)", @@ -248,14 +279,18 @@ void MacroActionTwitch::SendChatAnnouncement( void MacroActionTwitch::SetChatEmoteOnlyMode( const std::shared_ptr &token, bool enable) const { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "emote_mode", enable); - auto userId = token->GetUserID(); auto result = SendPatchRequest( *token, "https://api.twitch.tv", "/helix/chat/settings", - {{"broadcaster_id", userId}, {"moderator_id", userId}}, - data.Get()); + {{"broadcaster_id", *id}, {"moderator_id", *id}}, data.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to %s chat's emote-only mode! (%d)", @@ -265,9 +300,14 @@ void MacroActionTwitch::SetChatEmoteOnlyMode( void MacroActionTwitch::StartRaid(const std::shared_ptr &token) { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "from_broadcaster_id", - token->GetUserID().c_str()); + obs_data_set_string(data, "from_broadcaster_id", id->c_str()); obs_data_set_string(data, "to_broadcaster_id", _channel.GetUserID(*token).c_str()); auto result = SendPostRequest(*token, "https://api.twitch.tv", @@ -398,6 +438,12 @@ bool MacroActionTwitch::ResolveVariableSelectionToRewardId( void MacroActionTwitch::GetRewardInfo(const std::shared_ptr &token) { + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + if (_useVariableForRewardSelection && !ResolveVariableSelectionToRewardId(token)) { if (VerboseLoggingEnabled()) { @@ -416,7 +462,7 @@ void MacroActionTwitch::GetRewardInfo(const std::shared_ptr &token) } httplib::Params params = { - {"broadcaster_id", token->GetUserID()}, + {"broadcaster_id", *id}, {"id", _useVariableForRewardSelection ? _lastResolvedRewardId : _pointsReward.id}, }; diff --git a/plugins/twitch/macro-condition-twitch.cpp b/plugins/twitch/macro-condition-twitch.cpp index caa5b008..fed5d2ba 100644 --- a/plugins/twitch/macro-condition-twitch.cpp +++ b/plugins/twitch/macro-condition-twitch.cpp @@ -1096,6 +1096,12 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription( return; } + const auto id = token->GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + const auto channelID = _channel.GetUserID(*token); if (!TwitchChannel::IsValid(channelID)) { vblog(LOG_INFO, "skip %s because of invalid channel selection", @@ -1114,7 +1120,7 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription( if (includeModeratorId) { obs_data_set_string(condition, "moderator_user_id", - token->GetUserID().c_str()); + id->c_str()); } obs_data_apply(condition, extraConditions); diff --git a/plugins/twitch/tag-selection.cpp b/plugins/twitch/tag-selection.cpp index 6db4f599..f86e23b6 100644 --- a/plugins/twitch/tag-selection.cpp +++ b/plugins/twitch/tag-selection.cpp @@ -26,6 +26,12 @@ void TwitchTagList::Save(obs_data_t *obj) const void TwitchTagList::SetStreamTags(const TwitchToken &token) const { + const auto id = token.GetUserID(); + if (!id) { + vblog(LOG_INFO, "%s skip - invalid user id", __func__); + return; + } + nlohmann::json j; j["tags"] = toVector(); @@ -47,8 +53,7 @@ void TwitchTagList::SetStreamTags(const TwitchToken &token) const auto result = SendPatchRequest(token, "https://api.twitch.tv", "/helix/channels", - {{"broadcaster_id", token.GetUserID()}}, - j.dump()); + {{"broadcaster_id", *id}}, j.dump()); if (result.status != 204) { blog(LOG_INFO, "Failed to set stream tags! (%d)", diff --git a/plugins/twitch/token.cpp b/plugins/twitch/token.cpp index 3fcf4dfb..aac39a9c 100644 --- a/plugins/twitch/token.cpp +++ b/plugins/twitch/token.cpp @@ -206,7 +206,9 @@ void TwitchToken::Save(obs_data_t *obj) const { Item::Save(obj); obs_data_set_string(obj, "token", _token.c_str()); - obs_data_set_string(obj, "userID", _userID.c_str()); + if (_userID) { + obs_data_set_string(obj, "userID", _userID->c_str()); + } obs_data_set_bool(obj, "validateEventSubTimestamps", _validateEventSubTimestamps); obs_data_set_bool(obj, "warnIfInvalid", _warnIfInvalid); @@ -266,7 +268,7 @@ void TwitchToken::SetToken(const std::string &value) SendGetRequest(*this, "https://api.twitch.tv", "/helix/users"); if (res.status != 200) { blog(LOG_WARNING, "failed to get Twitch user id from token!"); - _userID = -1; + _userID = {}; return; } @@ -318,12 +320,35 @@ bool TwitchToken::IsValid(bool forceUpdate) const cli.Get("/oauth2/validate", httplib::Params{}, headers); _lastValidityCheckTime = std::chrono::system_clock::now(); _lastValidityCheckValue = _token; - _lastValidityCheckResult = response && response->status == 200; - if (!_lastValidityCheckResult) { + + if (!response || response->status != 200) { blog(LOG_INFO, "Twitch token %s is not valid!", _name.c_str()); + _lastValidityCheckResult = false; + return false; } - return _lastValidityCheckResult; + + OBSDataAutoRelease replyData = + obs_data_create_from_json(response->body.c_str()); + const char *id = obs_data_get_string(replyData, "user_id"); + if (!id) { + blog(LOG_INFO, + "Twitch token %s does validity check did not report user_id! Assume invalid!", + _name.c_str()); + _lastValidityCheckResult = false; + return false; + } + + if (_userID && _userID != id) { + blog(LOG_INFO, + "Twitch token %s does not match expected user (got %s, expected %s)!", + _name.c_str(), id, _userID->c_str()); + _lastValidityCheckResult = false; + return false; + } + + _lastValidityCheckResult = true; + return true; }; const bool tokenChanged = _lastValidityCheckValue != _token; @@ -729,6 +754,7 @@ void TwitchTokenSettingsDialog::RequestToken() auto scope = QString::fromStdString( generateScopeString(GetEnabledOptions())); + _validationTimer.stop(); _tokenGrabber.SetTokenScope(scope); _tokenGrabber.start(); _tokenStatus->setText(obs_module_text( @@ -766,6 +792,7 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional &value) Q_ARG(const QString &, name)); SetTokenInfoVisible(true); _requestToken->setEnabled(true); + _validationTimer.start(); } std::set TwitchTokenSettingsDialog::GetEnabledOptions() diff --git a/plugins/twitch/token.hpp b/plugins/twitch/token.hpp index f2e02cda..7f7acecf 100644 --- a/plugins/twitch/token.hpp +++ b/plugins/twitch/token.hpp @@ -51,7 +51,7 @@ public: void SetToken(const std::string &); bool IsEmpty() const { return _token.empty(); } std::optional GetToken() const; - std::string GetUserID() const { return _userID; } + std::optional GetUserID() const { return _userID; } std::shared_ptr GetEventSub(); bool ValidateTimestamps() const { return _validateEventSubTimestamps; } bool IsValid(bool forceUpdate = false) const; @@ -65,7 +65,7 @@ private: mutable std::string _lastValidityCheckValue; mutable bool _lastValidityCheckResult = false; mutable std::chrono::system_clock::time_point _lastValidityCheckTime; - std::string _userID; + std::optional _userID; std::set _tokenOptions = TokenOption::GetAllTokenOptions(); std::shared_ptr _eventSub; bool _validateEventSubTimestamps = false; diff --git a/plugins/twitch/twitch-tab.cpp b/plugins/twitch/twitch-tab.cpp index 7636d5eb..c7fa8653 100644 --- a/plugins/twitch/twitch-tab.cpp +++ b/plugins/twitch/twitch-tab.cpp @@ -153,6 +153,25 @@ static QStringList getCellLabels(TwitchToken *token, bool addName = true) return result; } +static void updateConnectionStatus(QTableWidget *table) +{ + for (int row = 0; row < table->rowCount(); row++) { + auto item = table->item(row, 0); + if (!item) { + continue; + } + + auto weakToken = GetWeakTwitchTokenByQString(item->text()); + auto token = weakToken.lock(); + if (!token) { + continue; + } + + UpdateItemTableRow(table, row, + getCellLabels(token.get(), false)); + } +} + static void openSettingsDialog() { auto selectedRows = @@ -174,6 +193,7 @@ static void openSettingsDialog() TwitchTokenSettingsDialog::AskForSettings(GetSettingsWindow(), *token.get()); + updateConnectionStatus(tabWidget->Table()); } static const QStringList headers = @@ -202,25 +222,6 @@ TwitchConnectionsTable::TwitchConnectionsTable(QTabWidget *parent) SetHelpVisible(GetTwitchTokens().empty()); } -static void updateConnectionStatus(QTableWidget *table) -{ - for (int row = 0; row < table->rowCount(); row++) { - auto item = table->item(row, 0); - if (!item) { - continue; - } - - auto weakToken = GetWeakTwitchTokenByQString(item->text()); - auto token = weakToken.lock(); - if (!token) { - continue; - } - - UpdateItemTableRow(table, row, - getCellLabels(token.get(), false)); - } -} - static QStringList getInvalidTokens() { QStringList tokens;