├── .gitignore ├── LICENSE ├── README.md └── TwitchAuthExample ├── Config.cs ├── Models └── Authorization.cs ├── Program.cs ├── TwitchAuthExample.csproj ├── TwitchAuthExample.sln └── WebServer.cs /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.suo 3 | *.user 4 | *.userosscache 5 | *.sln.docstates 6 | 7 | # Build results 8 | [Dd]ebug/ 9 | [Dd]ebugPublic/ 10 | [Rr]elease/ 11 | [Rr]eleases/ 12 | x64/ 13 | x86/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | *.vs/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | #NUNIT 28 | *.VisualState.xml 29 | TestResult.xml 30 | 31 | # Build Results of an ATL Project 32 | [Dd]ebugPS/ 33 | [Rr]eleasePS/ 34 | dlldata.c 35 | 36 | *_i.c 37 | *_p.c 38 | *_i.h 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.svclog 59 | *.scc 60 | 61 | # Chutzpah Test files 62 | _Chutzpah* 63 | 64 | # Visual C++ cache files 65 | ipch/ 66 | *.aps 67 | *.ncb 68 | *.opensdf 69 | *.sdf 70 | *.cachefile 71 | 72 | # Visual Studio profiler 73 | *.psess 74 | *.vsp 75 | *.vspx 76 | 77 | # TFS 2012 Local Workspace 78 | $tf/ 79 | 80 | # Guidance Automation Toolkit 81 | *.gpState 82 | 83 | # ReSharper is a .NET coding add-in 84 | _ReSharper*/ 85 | *.[Rr]e[Ss]harper 86 | *.DotSettings.user 87 | 88 | # JustCode is a .NET coding addin-in 89 | .JustCode 90 | 91 | # TeamCity is a build add-in 92 | _TeamCity* 93 | 94 | # DotCover is a Code Coverage Tool 95 | *.dotCover 96 | 97 | # NCrunch 98 | _NCrunch_* 99 | .*crunch*.local.xml 100 | 101 | # MightyMoose 102 | *.mm.* 103 | AutoTest.Net/ 104 | 105 | # Web workbench (sass) 106 | .sass-cache/ 107 | 108 | # Installshield output folder 109 | [Ee]xpress/ 110 | 111 | # DocProject is a documentation generator add-in 112 | DocProject/buildhelp/ 113 | DocProject/Help/*.HxT 114 | DocProject/Help/*.HxC 115 | DocProject/Help/*.hhc 116 | DocProject/Help/*.hhk 117 | DocProject/Help/*.hhp 118 | DocProject/Help/Html2 119 | DocProject/Help/html 120 | 121 | # Click-Once directory 122 | publish/ 123 | 124 | # Publish Web Output 125 | *.[Pp]ublish.xml 126 | *.azurePubxml 127 | # TODO: Comment the next line if you want to checkin your web deploy settings 128 | # but database connection strings (with potential passwords) will be unencrypted 129 | *.pubxml 130 | *.publishproj 131 | 132 | # NuGet Packages 133 | *.nupkg 134 | # The packages folder can be ignored because of Package Restore 135 | **/packages/* 136 | # except build/, which is used as an MSBuild target. 137 | !**/packages/build/ 138 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 139 | #!**/packages/repositories.config 140 | 141 | # Windows Azure Build Output 142 | csx/ 143 | *.build.csdef 144 | 145 | # Windows Store app package directory 146 | AppPackages/ 147 | 148 | # Others 149 | sql/ 150 | *.Cache 151 | ClientBin/ 152 | [Ss]tyle[Cc]op.* 153 | ~$* 154 | *~ 155 | *.dbmdl 156 | *.dbproj.schemaview 157 | *.pfx 158 | *.publishsettings 159 | node_modules/ 160 | 161 | # RIA/Silverlight projects 162 | Generated_Code/ 163 | 164 | # Backup & report files from converting an old project file 165 | # to a newer Visual Studio version. Backup files are not needed, 166 | # because we have git ;-) 167 | _UpgradeReport_Files/ 168 | Backup*/ 169 | UpgradeLog*.XML 170 | UpgradeLog*.htm 171 | 172 | # SQL Server files 173 | *.mdf 174 | *.ldf 175 | 176 | # Business Intelligence projects 177 | *.rdl.data 178 | *.bim.layout 179 | *.bim_*.settings 180 | 181 | # Microsoft Fakes 182 | FakesAssemblies/ 183 | 184 | # ========================= 185 | # Operating System Files 186 | # ========================= 187 | 188 | # OSX 189 | # ========================= 190 | 191 | .DS_Store 192 | .AppleDouble 193 | .LSOverride 194 | 195 | # Thumbnails 196 | ._* 197 | 198 | # Files that might appear on external disk 199 | .Spotlight-V100 200 | .Trashes 201 | 202 | # Directories potentially created on remote AFP share 203 | .AppleDB 204 | .AppleDesktop 205 | Network Trash Folder 206 | Temporary Items 207 | .apdisk 208 | 209 | # Windows 210 | # ========================= 211 | 212 | # Windows image file caches 213 | Thumbs.db 214 | ehthumbs.db 215 | 216 | # Folder config file 217 | Desktop.ini 218 | 219 | # Recycle Bin used on file shares 220 | $RECYCLE.BIN/ 221 | 222 | # Windows Installer files 223 | *.cab 224 | *.msi 225 | *.msm 226 | *.msp 227 | 228 | # Windows shortcuts 229 | *.lnk 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cole 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch Auth Example 2 | A C# example of how to use Twitch's [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication) to generate a user access token (as well as refresh token). We'll be using [TwitchLib.Api](https://github.com/TwitchLib/TwitchLib.Api) to make calls to Twitch. 3 | 4 | ## Overview 5 | Twitch's [OAuth implementation](https://dev.twitch.tv/docs/authentication) is fairly standard is does not deviate from [OAuth standards](https://oauth.net/2/). At a high level, you'll create an application on Twitch's developer site. This will automatically create a client id, client secret, and you'll have the chance to setup the redirect uri (where the user goes after authenticating on Twitch's auth portal). For this example, you should use "http://localhost:8080/redirect/". When the example application is run, a local web server will be started, listening on port 8080, and will capture the user being bounced to http://localhost:8080/redirect/ post authorization. Twitch sends them to the redirect uri address, along with a few query string parameters, including `code`. We will extract the `code` value from the query string, and use it, in combination with the client secret, client id, and redirect uri to complete the authorization flow with Twitch. Twitch will return a payload include `access_token`, `refresh_token`, `expires_in`, `scopes`, and a few other fields. Most important to us are the tokens and the expires in value. The access token expires at the end of the number of seconds provided in `expires_in`. You should use the refresh token (which does not expire unless the user revokes access to the application) to request a new access token. 6 | 7 | ### Developer Application 8 | The first step is to create an application on Twitch's developer portal: https://dev.twitch.tv . Login with your Twitch account. After being logged in, on the right side click Register Your Application. Give it a name, and an oAuth redirect url of `http://localhost:8080/redirect/`. Select a category, and hit the Create button. Click into the application to view its details. You'll find the Client Id (note this), as well as the button called New Secret. Click the button to generate a new secret and note it. At this point you should have a client id and client secret. 9 | 10 | ### Determine The Scope 11 | OAuth uses granular scope values to determine what kind of priviledges and data an application has access to. For Twitch, you can find the list of available scopes at: https://dev.twitch.tv/docs/authentication#scopes . Note down the scopes you're interested in. 12 | 13 | ### Setting Up The Example 14 | In the `Config.cs` file, you'll find three fields: `TwitchClientId`, `TwitchRedirectUri`, and `TwitchClientSecret`. Fill in these fields (uri is `http://localhost:8080/redirect/`). 15 | 16 | In `Program.cs`, around line 10, you'll find a private list variable named `scopes` containing a number of prepopulated scopes. Feel free to remove/update/change these to your liking. 17 | 18 | ### Run The Example 19 | Run the program. You'll see a couple things printed out, with the most recent being an authorization URL. Visit this URL. If you are not logged in, you'll be logged in. If you are logged in, you'll be asked to authorize your account against the application for the specified scopes. On authorization, you'll be bounced to `http://localhost:8080/redirect/` where you should be met with `Authorization started! Check your application!`. On the application, you'll find something along the lines of: 20 | ``` 21 | Authorization success! 22 | 23 | User: (id: ) 24 | Access token: 25 | Refresh token: 26 | Expires in: 27 | Scopes: 28 | ``` 29 | This example calls the Twitch api with the recently generated access token to discover the calling user (username, userid). 30 | 31 | ## Packages Used 32 | - [TwitchLib.Api](https://github.com/TwitchLib/TwitchLib.Api): C# Twitch api library that wraps all Twitch helix, v5, auth and undocumented endpoints, as well as some helper third party stuff. 33 | 34 | ## Contributors 35 | * Cole ([@swiftyspiffy](http://twitter.com/swiftyspiffy)) 36 | 37 | ## Questions? 38 | Ping me on Twitter: https://twitter.com/swiftyspiffy 39 | -------------------------------------------------------------------------------- /TwitchAuthExample/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TwitchAuthExample 6 | { 7 | public static class Config 8 | { 9 | public static readonly string TwitchClientId = ""; 10 | public static readonly string TwitchRedirectUri = "http://localhost:8080/redirect/"; 11 | public static readonly string TwitchClientSecret = ""; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /TwitchAuthExample/Models/Authorization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TwitchAuthExample.Models 6 | { 7 | public class Authorization 8 | { 9 | public string Code { get; } 10 | 11 | public Authorization(string code) 12 | { 13 | Code = code; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TwitchAuthExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using TwitchLib.Api.Core.Enums; 5 | 6 | namespace TwitchAuthExample 7 | { 8 | class Program 9 | { 10 | private static List scopes = new List { "chat:read", "whispers:read", "whispers:edit", "chat:edit", "channel:moderate" }; 11 | 12 | static void Main(string[] args) 13 | { 14 | MainAsync().GetAwaiter().GetResult(); 15 | } 16 | 17 | static async Task MainAsync() 18 | { 19 | Console.WriteLine("Twitch user access token example."); 20 | 21 | // ensure client id, secret, and redrect url are set 22 | validateCreds(); 23 | 24 | // create twitch api instance 25 | var api = new TwitchLib.Api.TwitchAPI(); 26 | api.Settings.ClientId = Config.TwitchClientId; 27 | 28 | // start local web server 29 | var server = new WebServer(Config.TwitchRedirectUri); 30 | 31 | // print out auth url 32 | Console.WriteLine($"Please authorize here:\n{getAuthorizationCodeUrl(Config.TwitchClientId, Config.TwitchRedirectUri, scopes)}"); 33 | 34 | // listen for incoming requests 35 | var auth = await server.Listen(); 36 | 37 | // exchange auth code for oauth access/refresh 38 | var resp = await api.Auth.GetAccessTokenFromCodeAsync(auth.Code, Config.TwitchClientSecret, Config.TwitchRedirectUri); 39 | 40 | // update TwitchLib's api with the recently acquired access token 41 | api.Settings.AccessToken = resp.AccessToken; 42 | 43 | // get the auth'd user 44 | var user = (await api.Helix.Users.GetUsersAsync()).Users[0]; 45 | 46 | // print out all the data we've got 47 | Console.WriteLine($"Authorization success!\n\nUser: {user.DisplayName} (id: {user.Id})\nAccess token: {resp.AccessToken}\nRefresh token: {resp.RefreshToken}\nExpires in: {resp.ExpiresIn}\nScopes: {string.Join(", ", resp.Scopes)}"); 48 | 49 | // refresh token 50 | var refresh = await api.Auth.RefreshAuthTokenAsync(resp.RefreshToken, Config.TwitchClientSecret); 51 | api.Settings.AccessToken = refresh.AccessToken; 52 | 53 | // confirm new token works 54 | user = (await api.Helix.Users.GetUsersAsync()).Users[0]; 55 | 56 | // print out all the data we've got 57 | Console.WriteLine($"Authorization success!\n\nUser: {user.DisplayName} (id: {user.Id})\nAccess token: {refresh.AccessToken}\nRefresh token: {refresh.RefreshToken}\nExpires in: {refresh.ExpiresIn}\nScopes: {string.Join(", ", refresh.Scopes)}"); 58 | 59 | // prevent console from closing 60 | Console.ReadLine(); 61 | } 62 | 63 | private static string getAuthorizationCodeUrl(string clientId, string redirectUri, List scopes) 64 | { 65 | var scopesStr = String.Join('+', scopes); 66 | 67 | return "https://id.twitch.tv/oauth2/authorize?" + 68 | $"client_id={clientId}&" + 69 | $"redirect_uri={System.Web.HttpUtility.UrlEncode(redirectUri)}&" + 70 | "response_type=code&" + 71 | $"scope={scopesStr}"; 72 | } 73 | 74 | private static void validateCreds() 75 | { 76 | if (String.IsNullOrEmpty(Config.TwitchClientId)) 77 | throw new Exception("client id cannot be null or empty"); 78 | if (String.IsNullOrEmpty(Config.TwitchClientSecret)) 79 | throw new Exception("client secret cannot be null or empty"); 80 | if (String.IsNullOrEmpty(Config.TwitchRedirectUri)) 81 | throw new Exception("redirect uri cannot be null or empty"); 82 | Console.WriteLine($"Using client id '{Config.TwitchClientId}', secret '{Config.TwitchClientSecret}' and redirect url '{Config.TwitchRedirectUri}'."); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /TwitchAuthExample/TwitchAuthExample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /TwitchAuthExample/TwitchAuthExample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30907.101 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchAuthExample", "TwitchAuthExample.csproj", "{A7D46DC3-E75E-4DC4-A1A8-FCC59C0519AC}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A7D46DC3-E75E-4DC4-A1A8-FCC59C0519AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A7D46DC3-E75E-4DC4-A1A8-FCC59C0519AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A7D46DC3-E75E-4DC4-A1A8-FCC59C0519AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A7D46DC3-E75E-4DC4-A1A8-FCC59C0519AC}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {683915CB-DDC6-4B89-BE62-B7FD9CD8AD94} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /TwitchAuthExample/WebServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using TwitchAuthExample.Models; 9 | 10 | namespace TwitchAuthExample 11 | { 12 | public class WebServer 13 | { 14 | private HttpListener listener; 15 | 16 | public WebServer(string uri) 17 | { 18 | listener = new HttpListener(); 19 | listener.Prefixes.Add(uri); 20 | } 21 | 22 | public async Task Listen() 23 | { 24 | listener.Start(); 25 | return await onRequest(); 26 | } 27 | 28 | private async Task onRequest() 29 | { 30 | while(listener.IsListening) 31 | { 32 | var ctx = await listener.GetContextAsync(); 33 | var req = ctx.Request; 34 | var resp = ctx.Response; 35 | 36 | using (var writer = new StreamWriter(resp.OutputStream)) 37 | { 38 | if (req.QueryString.AllKeys.Any("code".Contains)) 39 | { 40 | writer.WriteLine("Authorization started! Check your application!"); 41 | writer.Flush(); 42 | return new Models.Authorization(req.QueryString["code"]); 43 | } 44 | else 45 | { 46 | writer.WriteLine("No code found in query string!"); 47 | writer.Flush(); 48 | } 49 | } 50 | } 51 | return null; 52 | } 53 | } 54 | } 55 | --------------------------------------------------------------------------------