├── .gitignore ├── .travis.yml ├── README.md ├── TwitchLib.Extension.Core ├── Authentication │ ├── Events │ │ └── TokenValidatedContext.cs │ ├── TwitchExtensionAuthDefaults.cs │ ├── TwitchExtensionAuthExtensions.cs │ ├── TwitchExtensionAuthHandler.cs │ └── TwitchExtensionAuthOptions.cs ├── ExtensionsManager │ └── ExtensionManagerExtensions.cs └── TwitchLib.Extension.Core.csproj ├── TwitchLib.Extension.sln └── TwitchLib.Extension ├── Events └── SecretRotatedEventArgs.cs ├── Exceptions ├── BadParameterException.cs ├── BadRequestException.cs ├── BadResourceException.cs ├── BadScopeException.cs └── InvalidCredentialException.cs ├── Extension ├── ExtensionBase.cs ├── ExtensionConfiguration.cs ├── ExtensionManager.cs ├── Extensions.cs ├── ExtensionsConfiguration.cs ├── RotatedSecretExtension.cs └── StaticSecretExtension.cs ├── Models ├── CreateSecretRequest.cs ├── ExtensionPubSubRequest.cs ├── LiveChannel.cs ├── LiveChannels.cs ├── Secret.cs ├── Secrets.cs ├── SetExtensionBroadcasterOAuthReceiptRequest.cs ├── SetExtensionRequiredConfigurationRequest.cs └── TwitchLibJsonSerializer.cs └── TwitchLib.Extension.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | # Builds 401 | build/ 402 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 2.0.0 4 | dist: trusty 5 | script: 6 | - dotnet restore 7 | - dotnet build /home/travis/build/TwitchLib/TwitchLib.Extension/TwitchLib.Extension/TwitchLib.Extension.csproj --framework netstandard2.0 8 | - dotnet build /home/travis/build/TwitchLib/TwitchLib.Extension/TwitchLib.Extension.Core/TwitchLib.Extension.Core.csproj 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - sourceline: 'deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main' 14 | key_url: 'https://packages.microsoft.com/keys/microsoft.asc' 15 | packages: 16 | - dotnet-hostfxr-1.0.1 17 | - dotnet-sharedframework-microsoft.netcore.app-1.0.5 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## About 6 | TwitchLib is a powerful C# library that allows for interaction with various Twitch services. Currently supported services are: chat and whisper, API's (v3, v5, helix, undocumented, and third party), and PubSub event system. Below are the descriptions of the core components that make up TwitchLib. 7 | 8 | * **TwitchClient**: Handles chat and whisper Twitch services. Complete with a suite of events that fire for virtually every piece of data received from Twitch. Helper methods also exist for replying to whispers or fetching moderator lists. 9 | * **TwitchAPI**: Complete coverage of v3, v5, and Helix endpoints. The API is now a singleton class. This class allows fetching all publically accessable data as well as modify Twitch services like profiles and streams. 10 | * **TwitchPubSub**: Supports all documented Twitch PubSub topics as well as a few undocumented ones. 11 | 12 | ## Implementing 13 | Below are basic examples of how to utilize TwitchLib.Extension. These are C# examples, but this library can also be used in Visual Basic. 14 | This is meant to be utilized as part of a web based frontend, with that in mind the following examples are based on new Web applications built in Full framework 4.5.2 and above or dotnet core 2.0 and above. 15 | 16 | 17 | **NOTE**: This documentation is currently not final, support for the Extension Library wil be done via the Twitch API - Discord server 18 | 19 | ### Basics - full framework or dotnet core 20 | ```csharp 21 | using TwitchLib.Extension; 22 | 23 | //There are currently two types of extension which Extend the abstract ExtensionBase class 24 | //StaticSecretExtension & RotatedSecretExtension. StaticSecretExtension does not rotate the serect 25 | //away from the intial secret you set. RotatedSecretExtension rotates the secret based on the time interval you set 26 | //Twitch recommends a secret is reotated every 12 hours. More Extension types will be created in the future 27 | StaticSecretExtension staticExtension = new StaticSecretExtension(new ExtensionConfiguration 28 | { 29 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID}}", 30 | OwnerId = "{{THE_TWITCH_ID_OF_EXTENSION_OWNER}}", 31 | VersionNumber = "{{VERSION_NUMBER_YOU_ARE_USING}}",//e.g. 0.0.1 32 | StartingSecret = "{{YOUR_EXTENSION_SECRET}}" 33 | 34 | }); 35 | 36 | RotatedSecretExtension rotatedExtension = new RotatedSecretExtension(new ExtensionConfiguration 37 | { 38 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID}}", 39 | OwnerId = "{{THE_TWITCH_ID_OF_EXTENSION_OWNER}}", 40 | VersionNumber = "{{VERSION_NUMBER_YOU_ARE_USING}}",//e.g. 0.0.1 41 | StartingSecret = "{{YOUR_EXTENSION_SECRET}}" 42 | 43 | }, 720); 44 | 45 | //Verify a JWT 46 | string jwt = Request.Headers["x-extension-jwt"]; 47 | ClaimsPrincipal user = extension.Verify(jwt, out var validTokenOverlay); 48 | if (user == null) throw new Exception("Not valid"); 49 | 50 | //Creates a new secret and returns a list of the current available secrets 51 | //It's not recommended to use this method outside of an Extension instance. 52 | //It's recommended that a SecretHandler is created/used, we have created two for you 53 | //StaticSecretHandler or RotatedSecretHandler you can also implement the abstract SecretHandler class 54 | var secrets = await extension.CreateExtensionSecretAsync(); 55 | 56 | //Gets the current secret from the Extensions secret handler 57 | var currentSecret = extension.GetCurrentSecret(); 58 | 59 | //Gets the current list of secrets for the Extension from twitch 60 | secrets = await extension.GetExtensionSecretAsync(); 61 | 62 | //FOR EMERGENCY USE ONLY - deletes all extension secrets from twitch, your extension will no longer work 63 | var complete = await extension.RevokeExtensionSecretAsync(); 64 | 65 | 66 | var channels = await extension.GetLiveChannelsWithExtensionActivatedAsync(null); 67 | 68 | var channelId = "{{BROADCASTER_CHANNEL_ID}}"; //the channel id can be received from the current verified user principal; 69 | //var channelId = user.Claims.FirstOrDefault(y => y.Type == "channel_id").Value 70 | 71 | 72 | //Within your extension version management -> Extension Capabilities -> Required Configurations 73 | //When a braodcaster has "setup" the extension to your liking and it requires no further mandatory config 74 | //send the channelId and the string set in you version settings to signify this to twitch 75 | complete = await extension.SetExtensionRequiredConfigurationAsync(channelId, "{{WHATEVER_STRING_YOU_SET_IN_VERSION_MANAGEMENT}}"); 76 | 77 | 78 | //Within your extension version management -> Extension Capabilities -> Required Broadcaster Abilities 79 | //An OAuth process may occur, set your scopes in this section under Version Management 80 | //The broadcaster will go through the OAuth process set your Oauth callback in 81 | //Extension -> Settings-> General -> OAuth Redirect URI 82 | //Then follow the standard/twitch procedure to get an access_token 83 | //when complete pass true or when failed pass false using the method below 84 | complete = await extension.SetExtensionBroadcasterOAuthReceiptAsync(channelId, false); 85 | 86 | 87 | //You can send pubsub messages to your extension 88 | //see the twitch example for receiving these message 89 | //the below method send the message 90 | //It is up to you to determine permisions to send messages, the EBS or the current user can send messages 91 | //check https://dev.twitch.tv/docs/extensions/reference#jwt-schema (pubsub_perms) for more info 92 | //if a jwt is not passed it creates one based on the EBS sending the message 93 | complete = await extension.SendExtensionPubSubMessageAsync(channelId, new TwitchLib.Extension.Models.ExtensionPubSubRequest { 94 | Content_Type = "application/json", 95 | Targets = [ "broadcast"], 96 | Message = "{\"foo\":\"bar\"}" 97 | }); 98 | 99 | 100 | 101 | ``` 102 | 103 | 104 | 105 | ### .Net Core 106 | There is currently additional support for .Net core. 107 | 108 | #### Startup.cs 109 | ```csharp 110 | using TwitchLib.Extension; 111 | using TwitchLib.Extension.Core.Authentication; 112 | using TwitchLib.Extension.Core.ExtensionsManager; 113 | 114 | public void ConfigureServices(IServiceCollection services) 115 | { 116 | ..... 117 | services.AddAuthentication(options => 118 | { 119 | /*Options removed for space*/ 120 | }) 121 | .AddTwitchExtensionAuth();//<---- this is the bit you're after 122 | 123 | services.AddAuthorization(options => 124 | { 125 | options.AddPolicy("{{CREATE_A_POLICY_NAME}}", 126 | policy => policy.RequireClaim("extension_id", 127 | "{{INSERT_YOUR_EXTENSION_CLIENTID}}", 128 | "{{INSERT_YOUR_EXTENSION_CLIENTID2}}", 129 | ) 130 | ); 131 | }); 132 | 133 | //Add the extension manager functionality 134 | services.AddTwitchExtensionManager(); 135 | ..... 136 | } 137 | 138 | 139 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider) 140 | { 141 | ..... 142 | //This allows for multiple extensions. 143 | app.UseTwitchExtensionManager(serviceProvider, new Dictionary 144 | { 145 | { 146 | "{{INSERT_YOUR_EXTENSION_CLIENTID}}", 147 | new StaticSecretExtension( new ExtensionConfiguration { 148 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID}}", 149 | OwnerId= "{{THE_TWITCH_ID_OF_EXTENSION_OWNER}}", 150 | VersionNumber ="{{VERSION_NUMBER_YOU_ARE_USING}}",//e.g. 0.0.1 151 | SecretHandler = new StaticSecretHandler("{{YOUR_EXTENSION_SECRET}}") 152 | }) 153 | }, 154 | { 155 | "{{INSERT_YOUR_EXTENSION_CLIENTID2}}", 156 | new StaticSecretExtension( new ExtensionConfiguration { 157 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID2}}", 158 | OwnerId= "{{THE_TWITCH_ID_OF_EXTENSION_OWNER2}}", 159 | VersionNumber ="{{VERSION_NUMBER_YOU_ARE_USING2}}",//e.g. 0.0.1 160 | SecretHandler = new StaticSecretHandler("{{YOUR_EXTENSION_SECRET2}}") 161 | }) 162 | } 163 | }); 164 | 165 | 166 | 167 | ..... 168 | } 169 | ``` 170 | #### Example Controller 171 | ```csharp 172 | [Produces("application/json")] 173 | [Route("api/[Controller]")] 174 | [Authorize(AuthenticationSchemes = "TwitchExtensionAuth", Policy ="{{ENTER_THE_POLICY_USED_AT_STARTUP}}")] 175 | public class ExampleController : Controller 176 | { 177 | private readonly ExtensionManager _extensionManager; 178 | 179 | public ExampleController(ExtensionManager extensionManager) 180 | { 181 | _extensionManager = extensionManager; 182 | } 183 | 184 | [HttpGet] 185 | [Route("[Action]")] 186 | public async Task Puzzle() 187 | { 188 | //as you added the AddTwitchExtensionAuth in Startup and Authorize attribute on the controller the user is Authenticated and Authorized for you 189 | var user = User; 190 | 191 | //User Id - if user is not null but user_id is the user has not given permision to share their user_id 192 | user.Claims.FirstOrDefault(y => y.Type == "user_id") 193 | 194 | //Channel Id 195 | user.Claims.FirstOrDefault(y => y.Type == "channel_id") 196 | 197 | //Extension Id - This is added at Authorization and is the client id of the extension you were authorized against (if you have multiple extensions) 198 | user.Claims.FirstOrDefault(y => y.Type == "extension_id") 199 | 200 | //Other Claims - "exp", "opaque_user_id", "role", "pubsub_perms" 201 | 202 | 203 | var extensionId = user.Claims.FirstOrDefault(y => y.Type == "extension_id").Value; 204 | //call an extension method 205 | _extensionManager.GetExtension(extensionId).GetCurrentSecret(); //All Extension API methods available 206 | } 207 | } 208 | 209 | 210 | 211 | ``` 212 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/Authentication/Events/TokenValidatedContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.IdentityModel.Tokens; 4 | 5 | namespace TwitchLib.Extension.Core.Authentication.Events 6 | { 7 | public class TokenValidatedContext : ResultContext 8 | { 9 | public TokenValidatedContext( 10 | HttpContext context, 11 | AuthenticationScheme scheme, 12 | TwitchExtensionAuthOptions options) 13 | : base(context, scheme, options) {} 14 | 15 | public SecurityToken SecurityToken { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace TwitchLib.Extension.Core.Authentication 2 | { 3 | public static class TwitchExtensionAuthDefaults 4 | { 5 | public const string AuthenticationScheme = "TwitchExtensionAuth"; 6 | 7 | public static readonly string DisplayName = "TwitchExtensionAuth"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using System; 3 | 4 | namespace TwitchLib.Extension.Core.Authentication 5 | { 6 | public static class TwitchExtensionAuthExtensions 7 | { 8 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder) 9 | => builder.AddTwitchExtensionAuth(options => { }); 10 | 11 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder, Action configureOptions) 12 | => builder.AddTwitchExtensionAuth(TwitchExtensionAuthDefaults.AuthenticationScheme, configureOptions); 13 | 14 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) 15 | => builder.AddTwitchExtensionAuth(authenticationScheme, TwitchExtensionAuthDefaults.DisplayName, configureOptions); 16 | 17 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) 18 | => builder.AddScheme(authenticationScheme, displayName, configureOptions); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using TwitchLib.Extension.Core.Authentication.Events; 5 | using System.Text.Encodings.Web; 6 | using System.Threading.Tasks; 7 | 8 | namespace TwitchLib.Extension.Core.Authentication 9 | { 10 | public class TwitchExtensionAuthHandler : AuthenticationHandler 11 | { 12 | private readonly ExtensionManager _extensionManager; 13 | 14 | public TwitchExtensionAuthHandler(IOptionsMonitor options, 15 | ILoggerFactory logger, 16 | UrlEncoder encoder, 17 | ISystemClock clock, 18 | ExtensionManager extensionManager) : base(options, logger, encoder, clock) 19 | { 20 | _extensionManager = extensionManager; 21 | } 22 | 23 | protected override async Task HandleAuthenticateAsync() 24 | { 25 | string authorization = Request.Headers["x-extension-jwt"]; 26 | 27 | if (!string.IsNullOrEmpty(authorization)) 28 | { 29 | try 30 | { 31 | var user = _extensionManager.Verify(authorization, out var validTokenOverlay); 32 | 33 | if(user == null ) 34 | return AuthenticateResult.NoResult(); 35 | 36 | var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) 37 | { 38 | Principal = user, 39 | SecurityToken = validTokenOverlay 40 | }; 41 | 42 | tokenValidatedContext.Success(); 43 | return tokenValidatedContext.Result; 44 | } 45 | catch 46 | { 47 | return AuthenticateResult.NoResult(); 48 | } 49 | } 50 | 51 | return AuthenticateResult.NoResult(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | 3 | namespace TwitchLib.Extension.Core.Authentication 4 | { 5 | public class TwitchExtensionAuthOptions : AuthenticationSchemeOptions 6 | { 7 | public TwitchExtensionAuthOptions() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/ExtensionsManager/ExtensionManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace TwitchLib.Extension.Core.ExtensionsManager 8 | { 9 | public static class ExtensionManagerExtensions 10 | { 11 | public static IServiceCollection AddTwitchExtensionManager(this IServiceCollection services) 12 | { 13 | if (!services.Any(x => x.ServiceType == typeof(Extensions))) 14 | { 15 | services.AddSingleton(); 16 | } 17 | 18 | if (!services.Any(x => x.ServiceType == typeof(ExtensionManager))) 19 | { 20 | services.AddSingleton(); 21 | } 22 | return services; 23 | } 24 | 25 | public static IApplicationBuilder UseTwitchExtensionManager(this IApplicationBuilder app, IServiceProvider serviceProvider, IDictionary configuredExtensions) 26 | { 27 | var extensions = serviceProvider.GetService(); 28 | 29 | extensions.Extension = configuredExtensions; 30 | 31 | return app; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TwitchLib.Extension.Core/TwitchLib.Extension.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.1 4 | TwitchLib.Extension.Core 5 | 1.3.0 6 | Extension component of TwitchLib. This component expands the base Extension Library to include a dot net standard (core) Authentication Middleware and Extension Manager 7 | true 8 | luckyNoS7evin,thetestgame 9 | swiftyspiffy (cole) 10 | https://colejelinek.com/dev/twitchlib.png 11 | https://github.com/TwitchLib/TwitchLib.Extension 12 | https://opensource.org/licenses/MIT 13 | Copyright 2022 14 | Version 1 of the Extension library 15 | https://github.com/TwitchLib/TwitchLib.Extension 16 | Git 17 | twitch extension api c# csharp net core 2.1 authorization extension manager 18 | en-US 19 | 2.2.0.0 20 | 2.2.0.0 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /TwitchLib.Extension.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Extension", "TwitchLib.Extension\TwitchLib.Extension.csproj", "{3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Extension.Core", "TwitchLib.Extension.Core\TwitchLib.Extension.Core.csproj", "{5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {8E06B51D-BBC5-431C-A0FE-1B967CA5AB44} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Events/SecretRotatedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitchLib.Extension.Events 4 | { 5 | public class SecretRotatedEventArgs : EventArgs 6 | { 7 | public string NewSecret { get; set; } 8 | public DateTime Expires { get; set; } 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Exceptions/BadParameterException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitchLib.Extension.Exceptions 4 | { 5 | /// Exception representing an invalid resource 6 | public class BadParameterException : Exception 7 | { 8 | /// Exception constructor 9 | public BadParameterException(string badParamData) 10 | : base(badParamData) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Exceptions/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitchLib.Extension.Exceptions 4 | { 5 | /// Exception representing a request that doesn't have a clientid attached. 6 | public class BadRequestException : Exception 7 | { 8 | /// Exception constructor 9 | public BadRequestException(string apiData) 10 | : base(apiData) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Exceptions/BadResourceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitchLib.Extension.Exceptions 4 | { 5 | /// Exception representing an invalid resource 6 | public class BadResourceException : Exception 7 | { 8 | /// Exception constructor 9 | public BadResourceException(string apiData) 10 | : base(apiData) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /TwitchLib.Extension/Exceptions/BadScopeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitchLib.Extension.Exceptions 4 | { 5 | /// Exception representing a provided scope was not permitted. 6 | public class BadScopeException : Exception 7 | { 8 | /// Exception constructor 9 | public BadScopeException(string data) 10 | : base(data) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /TwitchLib.Extension/Exceptions/InvalidCredentialException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitchLib.Extension.Exceptions 4 | { 5 | /// Exception representing a detection that sent credentials were invalid. 6 | public class InvalidCredentialException : Exception 7 | { 8 | /// Exception constructor 9 | public InvalidCredentialException(string data) 10 | : base(data) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/ExtensionBase.cs: -------------------------------------------------------------------------------- 1 | using JWT; 2 | using JWT.Algorithms; 3 | using JWT.Serializers; 4 | using Microsoft.IdentityModel.Tokens; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Security.Claims; 12 | using System.Threading.Tasks; 13 | using TwitchLib.Extension.Exceptions; 14 | using TwitchLib.Extension.Models; 15 | 16 | namespace TwitchLib.Extension 17 | { 18 | public abstract class ExtensionBase 19 | { 20 | private const string _extensionUrl = "https://api.twitch.tv/helix/extensions/{0}"; 21 | private readonly ExtensionConfiguration _config; 22 | protected IEnumerable Secrets { get; set; } 23 | 24 | public string CurrentSecret { get => Secrets.ToList().OrderByDescending(x => x.Expires).First().Content; } 25 | 26 | public ExtensionBase(ExtensionConfiguration config) 27 | { 28 | _config = config; 29 | Secrets = new List { new Secret(config.StartingSecret, DateTime.Now, DateTime.Now.AddYears(100)) }; 30 | } 31 | 32 | /// 33 | /// Creates a new secret for a specified extension. Also rotates any current secrets out of service, with enough 34 | /// time for extension clients to gracefully switch over to the new secret. The delay period, 35 | /// between the generation of the new secret and its use by Twitch, is specified by a required parameter, activation_delay_secs. 36 | /// The default delay is 300 (5 minutes); if a value less than this is specified, Twitch uses 300. 37 | /// 38 | /// Use this function only when you are ready to install the new secret it returns. 39 | /// 40 | /// How long Twitch should wait before using your new secret and rolling it out to users 41 | /// Data object containing all of current extension secrets that are valid and haven't expired 42 | public virtual async Task CreateExtensionSecretAsync(int activationDelaySeconds = 300) => 43 | await CreateExtensionSecretAsync(CurrentSecret, _config.Id, _config.OwnerId, activationDelaySeconds); 44 | 45 | /// 46 | /// Retrieves a specified extension’s secret data: a version and an array of secret objects. 47 | /// Each secret object returned contains a base64-encoded secret, a UTC timestamp when the secret becomes active, 48 | /// and a timestamp when the secret expires. 49 | /// 50 | /// Data object containing all of current extension secrets that are valid and haven't expired 51 | public virtual async Task GetExtensionSecretAsync() => 52 | await GetExtensionSecretAsync(CurrentSecret, _config.Id, _config.OwnerId); 53 | 54 | /// 55 | /// Deletes all secrets associated with a specified extension. 56 | /// 57 | /// This immediately breaks all clients until both a new Create Extension Secret is executed 58 | /// and the clients manually refresh themselves. 59 | /// 60 | /// Use this only if a secret is compromised and must be removed immediately from circulation. 61 | /// 62 | /// true if secrets were successfully revoked 63 | public virtual async Task RevokeExtensionSecretAsync() 64 | { 65 | return await RevokeExtensionSecretAsync( 66 | CurrentSecret, 67 | _config.Id, 68 | _config.OwnerId); 69 | } 70 | 71 | /// 72 | /// Returns one page of live channels that have installed and activated a specified extension. 73 | /// 74 | /// A channel that just went live may take a few minutes to appear in this list, and a channel may continue to 75 | /// appear on this list for a few minutes after it stops broadcasting. 76 | /// 77 | /// 78 | /// List of channels that are live with the extension installed 79 | public virtual async Task GetLiveChannelsWithExtensionActivatedAsync(string cursor = null) 80 | { 81 | return await GetLiveChannelsWithExtensionActivatedAsync( 82 | CurrentSecret, 83 | _config.Id, 84 | _config.OwnerId, 85 | cursor); 86 | } 87 | 88 | /// 89 | /// Enable activation of a specified extension, after any required broadcaster configuration is correct. 90 | /// This is for extensions that require broadcaster configuration before activation. 91 | /// 92 | /// The Twitch channel ID we are setting the specified value for 93 | /// 94 | /// true if requiredConfiguration was set successfully 95 | public virtual async Task SetExtensionRequiredConfigurationAsync(string channelId, string requiredConfiguration) 96 | { 97 | return await SetExtensionRequiredConfigurationAsync( 98 | CurrentSecret, 99 | _config.Id, 100 | _config.VersionNumber, 101 | _config.OwnerId, 102 | channelId, 103 | requiredConfiguration); 104 | } 105 | 106 | /// 107 | /// Indicates whether the broadcaster allowed the permissions your extension requested, 108 | /// through a required permissions_received parameter. The endpoint URL includes the channel ID 109 | /// of the page where the extension is iframe embedded. 110 | /// 111 | /// The Twitch channel ID we are setting the specified value for 112 | /// 113 | /// true if permissionsReceived was set successfully 114 | public virtual async Task SetExtensionBroadcasterOAuthReceiptAsync(string channelId, bool permissionsReceived) 115 | { 116 | return await SetExtensionBroadcasterOAuthReceiptAsync( 117 | CurrentSecret, 118 | _config.Id, 119 | _config.VersionNumber, 120 | _config.OwnerId, 121 | channelId, 122 | permissionsReceived); 123 | } 124 | 125 | /// 126 | /// Twitch provides a publish-subscribe system for your EBS (Extension Back-end Service) to communicate 127 | /// with both the broadcaster and viewers. Calling this endpoint forwards your message using the same 128 | /// mechanism as the send() function in the JavaScript helper API. 129 | /// 130 | /// The Twitch channel ID we are sending the message for 131 | /// 132 | /// Optional JWT of user, this JWT should only be those passed by twitch in the x-extension-jwt header 133 | /// true if PubSub message successfully sent 134 | public virtual async Task SendExtensionPubSubMessageAsync(string channelId, Models.ExtensionPubSubRequest message, string jwt = null) 135 | { 136 | return await SendExtensionPubSubMessageAsync( 137 | CurrentSecret, 138 | _config.Id, 139 | _config.OwnerId, 140 | channelId, 141 | message, 142 | jwt); 143 | } 144 | 145 | protected async Task CreateExtensionSecretAsync(string extensionSecret, string extensionId, string extensionOwnerId, int activationDelaySeconds = 300) 146 | { 147 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 148 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 149 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 150 | if (activationDelaySeconds < 300) throw new BadParameterException("The activation delay in seconds is not allowed to be less than 300"); 151 | 152 | var url = $"jwt/secrets?extension_id={extensionId}"; 153 | var request = new CreateSecretRequest { Activation_Delay_Secs = activationDelaySeconds }; 154 | return ExtensionSecretsData.FromJson((await RequestAsync(extensionSecret, url, "POST", extensionOwnerId, extensionId, request.ToJson()).ConfigureAwait(false)).Value); 155 | } 156 | 157 | protected async Task GetExtensionSecretAsync(string extensionSecret, string extensionId, string extensionOwnerId) 158 | { 159 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 160 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 161 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 162 | 163 | var url = $"jwt/secrets?extension_id={extensionId}"; 164 | return ExtensionSecretsData.FromJson((await RequestAsync(extensionSecret, url, "GET", extensionOwnerId, extensionId).ConfigureAwait(false)).Value); 165 | } 166 | 167 | protected async Task RevokeExtensionSecretAsync(string extensionSecret, string extensionId, string extensionOwnerId) 168 | { 169 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 170 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 171 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 172 | 173 | var url = $"jwt/secrets?extension_id={extensionId}"; 174 | return (await RequestAsync(extensionSecret, url, "DELETE", extensionOwnerId, extensionId).ConfigureAwait(false)).Key == 204; 175 | } 176 | 177 | protected async Task GetLiveChannelsWithExtensionActivatedAsync(string extensionSecret, string extensionId,string extensionOwnerId, string cursor = null) 178 | { 179 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 180 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 181 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 182 | 183 | var url = $"/live?extension_id{extensionId}"; 184 | if (!string.IsNullOrWhiteSpace(cursor)) 185 | url += $"?after={cursor}"; 186 | return LiveChannels.FromJson((await RequestAsync(extensionSecret, url, "GET", extensionOwnerId, extensionId).ConfigureAwait(false)).Value); 187 | } 188 | 189 | protected async Task SetExtensionRequiredConfigurationAsync(string extensionSecret, string extensionId, string extensionVersion, string extensionOwnerId, string channelId, string requiredConfiguration) 190 | { 191 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 192 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 193 | if (string.IsNullOrWhiteSpace(extensionVersion)) throw new BadParameterException("The extension version is not valid. It is not allowed to be null, empty or filled with whitespaces."); 194 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 195 | if (string.IsNullOrWhiteSpace(channelId)) throw new BadParameterException("The channel id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 196 | if (string.IsNullOrEmpty(requiredConfiguration)) throw new BadParameterException("The required configuration is not valid. It is not allowed to be null or empty."); 197 | 198 | var url = $"required_configuration?broadcaster_id={channelId}"; 199 | var request = new SetExtensionRequiredConfigurationRequest 200 | { 201 | RequiredConfiguration = requiredConfiguration, 202 | ExtensionId = extensionId, 203 | ExtensionVersion = extensionVersion, 204 | }; 205 | 206 | return (await RequestAsync(extensionSecret, url, "PUT", extensionOwnerId, extensionId, request.ToJson()).ConfigureAwait(false)).Key == 204; 207 | } 208 | 209 | protected async Task SetExtensionBroadcasterOAuthReceiptAsync(string extensionSecret, string extensionId, string extensionVersion, string extensionOwnerId, string channelId, bool permissionsReceived) 210 | { 211 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 212 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 213 | if (string.IsNullOrWhiteSpace(extensionVersion)) throw new BadParameterException("The extension version is not valid. It is not allowed to be null, empty or filled with whitespaces."); 214 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 215 | if (string.IsNullOrWhiteSpace(channelId)) throw new BadParameterException("The channel id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 216 | 217 | var url = $"{extensionId}/{extensionVersion}/oauth_receipt?channel_id={channelId}"; 218 | var request = new SetExtensionBroadcasterOAuthReceiptRequest { Permissions_Received = permissionsReceived }; 219 | return (await RequestAsync(extensionSecret, url, "PUT", extensionOwnerId, extensionId, request.ToJson()).ConfigureAwait(false)).Key == 204; 220 | } 221 | 222 | protected async Task SendExtensionPubSubMessageAsync(string extensionSecret, string extensionId, string extensionOwnerId, string channelId, Models.ExtensionPubSubRequest message, string jwt =null) 223 | { 224 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces."); 225 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 226 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 227 | if (string.IsNullOrWhiteSpace(channelId)) throw new BadParameterException("The channel id is not valid. It is not allowed to be null, empty or filled with whitespaces."); 228 | 229 | if (string.IsNullOrEmpty(jwt)) jwt = Sign(extensionSecret, extensionOwnerId, 10, channelId); 230 | return (await RequestAsync(extensionSecret, "pubsub", "POST", extensionOwnerId, extensionId, message.ToJson(), jwt).ConfigureAwait(false)).Key == 204; 231 | } 232 | 233 | private async Task> RequestAsync(string secret, string url, string method, string userId, string clientId, object payload=null, string jwt = null) 234 | { 235 | var request = WebRequest.CreateHttp(string.Format(_extensionUrl, url)); 236 | 237 | request.Headers["Client-Id"] = clientId; 238 | request.Method = method; 239 | request.ContentType = "application/json"; 240 | var token = jwt ?? Sign(secret, userId, 10); 241 | request.Headers["Authorization"] = $"Bearer {token}"; 242 | 243 | if (payload != null) 244 | using (var writer = new StreamWriter(await request.GetRequestStreamAsync())) 245 | writer.Write(payload); 246 | try 247 | { 248 | var response = (HttpWebResponse)request.GetResponse(); 249 | using (var reader = new StreamReader(response.GetResponseStream())) 250 | { 251 | string data = reader.ReadToEnd(); 252 | return new KeyValuePair((int)response.StatusCode, data); 253 | } 254 | } 255 | catch (WebException ex) { HandleWebException(ex); } 256 | 257 | return new KeyValuePair(0, null); 258 | } 259 | 260 | 261 | private void HandleWebException(WebException e) 262 | { 263 | HttpWebResponse errorResp = e.Response as HttpWebResponse; 264 | if (errorResp == null) 265 | throw e; 266 | switch (errorResp.StatusCode) 267 | { 268 | case HttpStatusCode.BadRequest: 269 | throw new BadRequestException("Your request failed because your ClientID was invalid/not set."); 270 | case HttpStatusCode.Unauthorized: 271 | throw new BadScopeException("Your request was blocked due to bad credentials (do you have the right scope for your access token?)."); 272 | case HttpStatusCode.NotFound: 273 | throw new BadResourceException("The resource you tried to access was not valid."); 274 | default: 275 | throw e; 276 | } 277 | } 278 | 279 | 280 | #region JWTSignAndVerify 281 | public ClaimsPrincipal Verify(string jwt, out SecurityToken validTokenOverlay) 282 | { 283 | ClaimsPrincipal user = null; 284 | validTokenOverlay = null; 285 | foreach (var secret in Secrets.ToList().OrderByDescending(x => x.Expires).Where(x => x.Expires > DateTime.Now)) 286 | { 287 | user = VerifyWithSecret(jwt, secret.Content, out validTokenOverlay); 288 | if (user != null) 289 | { 290 | ((ClaimsIdentity)user.Identity).AddClaim(new Claim("extension_id", _config.Id, ClaimValueTypes.String)); 291 | break; 292 | } 293 | } 294 | return user; 295 | } 296 | 297 | private ClaimsPrincipal VerifyWithSecret(string jwt, string secret, out SecurityToken validTokenOverlay) 298 | { 299 | var validationParameters = new TokenValidationParameters 300 | { 301 | IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(secret)), 302 | ValidateAudience = false, 303 | ValidateLifetime = false, 304 | ValidateIssuer = false, 305 | ValidateIssuerSigningKey = true 306 | }; 307 | 308 | var handler = new JwtSecurityTokenHandler(); 309 | 310 | try 311 | { 312 | return handler.ValidateToken(jwt, validationParameters, out validTokenOverlay); 313 | } 314 | catch 315 | { 316 | validTokenOverlay = null; 317 | return null; 318 | } 319 | } 320 | 321 | private string Sign(string secret, string userId, int expirySeconds) 322 | { 323 | var payload = new Dictionary 324 | { 325 | { "exp", (GetEpoch() + expirySeconds) }, 326 | { "user_id", userId }, 327 | { "role", "external" } 328 | }; 329 | 330 | IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); 331 | IJsonSerializer serializer = new JsonNetSerializer(); 332 | IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 333 | IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); 334 | 335 | var token = encoder.Encode(payload, Convert.FromBase64String(secret)); 336 | return token; 337 | } 338 | 339 | class perms 340 | { 341 | public string[] send; 342 | } 343 | private string Sign(string secret, string userId, int expirySeconds, string channelId) 344 | { 345 | var perms = new perms { send = new string[] { "*" } }; 346 | 347 | var payload = new Dictionary 348 | { 349 | { "exp", (GetEpoch() + expirySeconds) }, 350 | { "user_id", userId }, 351 | { "role", "external" }, 352 | { "channel_id", channelId }, 353 | { "pubsub_perms", perms } 354 | }; 355 | 356 | IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); 357 | IJsonSerializer serializer = new JsonNetSerializer(); 358 | IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 359 | IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); 360 | 361 | var token = encoder.Encode(payload, Convert.FromBase64String(secret)); 362 | return token; 363 | } 364 | 365 | private int GetEpoch() 366 | { 367 | TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1); 368 | int secondsSinceEpoch = (int)t.TotalSeconds; 369 | return secondsSinceEpoch; 370 | } 371 | 372 | #endregion 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/ExtensionConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace TwitchLib.Extension 2 | { 3 | public class ExtensionConfiguration 4 | { 5 | public string Id { get; set; } 6 | public string OwnerId { get; set; } 7 | public string VersionNumber { get; set; } 8 | public string StartingSecret { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/ExtensionManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Tokens; 2 | using System.Security.Claims; 3 | 4 | namespace TwitchLib.Extension 5 | { 6 | public class ExtensionManager 7 | { 8 | private Extensions _extensions; 9 | public ExtensionManager(Extensions extensions) 10 | { 11 | _extensions = extensions; 12 | } 13 | 14 | public ClaimsPrincipal Verify(string jwt, out SecurityToken validTokenOverlay) 15 | { 16 | ClaimsPrincipal user = null; 17 | validTokenOverlay = null; 18 | 19 | foreach (var extension in _extensions.Extension.Values) 20 | { 21 | user = extension.Verify(jwt, out validTokenOverlay); 22 | if (user != null) 23 | { 24 | break; 25 | } 26 | } 27 | 28 | return user; 29 | } 30 | 31 | public ExtensionBase GetExtension(string extensionId) 32 | { 33 | return _extensions.Extension[extensionId]; 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace TwitchLib.Extension 4 | { 5 | public class Extensions 6 | { 7 | public IDictionary Extension { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/ExtensionsConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace TwitchLib.Extension 4 | { 5 | public class ExtensionsConfiguration 6 | { 7 | public IEnumerable Extensions { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/RotatedSecretExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using TwitchLib.Extension.Events; 5 | 6 | namespace TwitchLib.Extension 7 | { 8 | public class RotatedSecretExtension : ExtensionBase 9 | { 10 | private readonly int _interval; 11 | public event EventHandler SecretRotated; 12 | 13 | public RotatedSecretExtension(ExtensionConfiguration config, int rotationIntervalMinutes = 720) : base(config) 14 | { 15 | _interval = rotationIntervalMinutes * 1000 * 60; 16 | var secretsData = GetExtensionSecretAsync().GetAwaiter().GetResult(); 17 | if (secretsData != null) 18 | Secrets = secretsData.Data.SelectMany(x => x.Secrets).ToList(); 19 | 20 | Task.Run(async () => 21 | { 22 | while (true) 23 | { 24 | await Task.Delay(_interval); 25 | 26 | if (SecretRotated != null) 27 | { 28 | secretsData = await CreateExtensionSecretAsync(); 29 | if (secretsData == null) return; 30 | 31 | Secrets = secretsData.Data.SelectMany(x => x.Secrets).ToList(); 32 | var currentSecret = Secrets.ToList().OrderByDescending(x => x.Expires).First(); 33 | OnSecretRotated(new SecretRotatedEventArgs 34 | { 35 | NewSecret = currentSecret.Content, 36 | Expires = currentSecret.Expires 37 | }); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | protected virtual void OnSecretRotated(SecretRotatedEventArgs e) 44 | { 45 | SecretRotated?.Invoke(this, e); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Extension/StaticSecretExtension.cs: -------------------------------------------------------------------------------- 1 | namespace TwitchLib.Extension 2 | { 3 | public class StaticSecretExtension : ExtensionBase 4 | { 5 | public StaticSecretExtension(ExtensionConfiguration config) : base(config) 6 | { } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/CreateSecretRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public class CreateSecretRequest 6 | { 7 | [JsonProperty(PropertyName = "activation_delay_secs")] 8 | public int Activation_Delay_Secs { get; internal set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/ExtensionPubSubRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public class ExtensionPubSubRequest 6 | { 7 | [JsonProperty(PropertyName = "message")] 8 | public object Message { get; set; } 9 | [JsonProperty(PropertyName = "target")] 10 | public string[] Targets { get; set; } 11 | [JsonProperty(PropertyName = "broadcaster_id")] 12 | public string BroadcasterId { get; set; } = null; 13 | [JsonProperty(PropertyName = "is_global_broadcast")] 14 | public bool IsGlobalBroadcast => string.IsNullOrEmpty(this.BroadcasterId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/LiveChannel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public class LiveChannel 6 | { 7 | [JsonProperty(PropertyName = "broadcaster_id")] 8 | public string BroadcasterId { get; protected set; } 9 | [JsonProperty(PropertyName = "broadcaster_name")] 10 | public string BroadcasterName { get; protected set; } 11 | [JsonProperty(PropertyName = "game_name")] 12 | public string GameName { get; protected set; } 13 | [JsonProperty(PropertyName = "game_id")] 14 | public string GameId { get; protected set; } 15 | [JsonProperty(PropertyName = "title")] 16 | public string Title { get; protected set; } 17 | [JsonProperty(PropertyName = "view_count")] 18 | public int View_Count { get; protected set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/LiveChannels.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public partial class LiveChannels 6 | { 7 | [JsonProperty(PropertyName = "channels")] 8 | public LiveChannel[] Channels { get; protected set; } 9 | [JsonProperty(PropertyName = "cursor")] 10 | public string Cursor { get; protected set; } 11 | } 12 | 13 | public partial class LiveChannels 14 | { 15 | public static LiveChannels FromJson(string json) => JsonConvert.DeserializeObject(json, TwitchLibJsonSerializer.Settings); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/Secret.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace TwitchLib.Extension.Models 5 | { 6 | public class Secret 7 | { 8 | public Secret() 9 | { } 10 | 11 | public Secret(string content, DateTime active, DateTime expires) 12 | { 13 | Content = content; 14 | Active = active; 15 | Expires = expires; 16 | } 17 | 18 | [JsonProperty(PropertyName = "active_at")] 19 | public DateTime Active { get; protected set; } 20 | [JsonProperty(PropertyName = "content")] 21 | public string Content { get; protected set; } 22 | [JsonProperty(PropertyName = "expires_at")] 23 | public DateTime Expires { get; protected set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/Secrets.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public partial class ExtensionSecrets 6 | { 7 | [JsonProperty(PropertyName = "format_version")] 8 | public string Format_Version { get; protected set; } 9 | [JsonProperty(PropertyName = "secrets")] 10 | public Secret[] Secrets { get; protected set; } 11 | } 12 | 13 | public partial class ExtensionSecretsData 14 | { 15 | [JsonProperty(PropertyName = "data")] 16 | public ExtensionSecrets[] Data { get; protected set; } 17 | } 18 | 19 | public partial class ExtensionSecretsData 20 | { 21 | public static ExtensionSecretsData FromJson(string json) => JsonConvert.DeserializeObject(json, TwitchLibJsonSerializer.Settings); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/SetExtensionBroadcasterOAuthReceiptRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public class SetExtensionBroadcasterOAuthReceiptRequest 6 | { 7 | [JsonProperty(PropertyName = "permissions_received")] 8 | public bool Permissions_Received { get; internal set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/SetExtensionRequiredConfigurationRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TwitchLib.Extension.Models 4 | { 5 | public class SetExtensionRequiredConfigurationRequest 6 | { 7 | [JsonProperty(PropertyName = "required_configuration")] 8 | public string RequiredConfiguration { get; internal set; } 9 | [JsonProperty(PropertyName = "extension_id")] 10 | public string ExtensionId { get; internal set; } 11 | [JsonProperty(PropertyName = "extension_version")] 12 | public string ExtensionVersion { get; internal set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TwitchLib.Extension/Models/TwitchLibJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | 4 | namespace TwitchLib.Extension.Models 5 | { 6 | internal static class TwitchLibJsonSerializer 7 | { 8 | public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings 9 | { 10 | ContractResolver = new LowercaseContractResolver(), 11 | NullValueHandling = NullValueHandling.Ignore 12 | }; 13 | 14 | public static string ToJson(this T self) => JsonConvert.SerializeObject(self, Formatting.Indented, Settings); 15 | 16 | public class LowercaseContractResolver : DefaultContractResolver 17 | { 18 | protected override string ResolvePropertyName(string propertyName) 19 | { 20 | return propertyName.ToLower(); 21 | } 22 | } 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TwitchLib.Extension/TwitchLib.Extension.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.1;net46 4 | TwitchLib.Extension 5 | 1.3.0 6 | Extension component of TwitchLib. This component allows you to access Twitch's Extension Api system 7 | true 8 | luckyNoS7evin,thetestgame 9 | swiftyspiffy (cole) 10 | https://colejelinek.com/dev/twitchlib.png 11 | https://github.com/TwitchLib/TwitchLib.Extension 12 | https://opensource.org/licenses/MIT 13 | Copyright 2022 14 | Version 1 of the Extension library 15 | https://github.com/TwitchLib/TwitchLib.Extension 16 | Git 17 | twitch extension api c# csharp net core 2.1 18 | en-US 19 | 2.2.0.0 20 | 2.2.0.0 21 | ../build/TwitchLib.Extension 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | NET46 31 | 32 | 33 | NETSTANDARD 34 | 35 | 36 | 37 | --------------------------------------------------------------------------------