├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── SteamAuth ├── APIEndpoints.cs ├── AuthenticatorLinker.cs ├── Confirmation.cs ├── CookieAwareWebClient.cs ├── Properties │ └── AssemblyInfo.cs ├── SessionData.cs ├── SteamAuth.csproj ├── SteamAuth.sln ├── SteamGuardAccount.cs ├── SteamWeb.cs └── TimeAligner.cs └── TestBed ├── App.config ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── TestBed.csproj └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /.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/master/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 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Joshua Coffey 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SteamAuth 2 | A C# library that provides vital Steam Mobile Authenticator functionality. **Looking for a desktop client to act as a Steam Mobile Authenticator? [Check out SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator)** 3 | 4 | # Functionality 5 | Currently, this library can: 6 | 7 | * Generate login codes for a given Shared Secret 8 | * Login to a user account 9 | * Link and activate a new mobile authenticator to a user account after logging in 10 | * Remove itself from an account 11 | * Fetch, accept, and deny mobile confirmations 12 | 13 | # Requirements 14 | 15 | * [Newtonsoft.Json](http://www.newtonsoft.com/json) 16 | 17 | # Usage 18 | To generate login codes if you already have a Shared Secret, simply instantiate a `SteamGuardAccount` and set its `SharedSecret`. Then call `SteamGuardAccount.GenerateSteamGuardCode()`. 19 | 20 | To add a mobile authenticator to a user, instantiate a `UserLogin` instance which will allow you to login to the account. After logging in, instantiate an `AuthenticatorLinker` and use `AuthenticatorLinker.AddAuthenticator()` and `AuthenticatorLinker.FinalizeAddAuthenticator()` to link a new authenticator. **After calling AddAuthenticator(), and before calling FinalizeAddAuthenticator(), please save a JSON string of the `AuthenticatorLinker.LinkedAccount`. This will contain everything you need to generate subsequent codes. Failing to do this will lock you out of your account.** 21 | 22 | To fetch mobile confirmations, call `SteamGuardAccount.FetchConfirmations()`. You can then call `SteamGuardAccount.AcceptConfirmation` and `SteamGuardAccount.DenyConfirmation`. 23 | 24 | # Upcoming Features 25 | In order to be feature complete, this library will: 26 | 27 | * Be better documented (feature!!) 28 | 29 | 30 | -------------------------------------------------------------------------------- /SteamAuth/APIEndpoints.cs: -------------------------------------------------------------------------------- 1 | namespace SteamAuth 2 | { 3 | public static class APIEndpoints 4 | { 5 | public const string STEAMAPI_BASE = "https://api.steampowered.com"; 6 | public const string COMMUNITY_BASE = "https://steamcommunity.com"; 7 | public const string MOBILEAUTH_BASE = STEAMAPI_BASE + "/IMobileAuthService/%s/v0001"; 8 | public static string MOBILEAUTH_GETWGTOKEN = MOBILEAUTH_BASE.Replace("%s", "GetWGToken"); 9 | public const string TWO_FACTOR_BASE = STEAMAPI_BASE + "/ITwoFactorService/%s/v0001"; 10 | public static string TWO_FACTOR_TIME_QUERY = TWO_FACTOR_BASE.Replace("%s", "QueryTime"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SteamAuth/AuthenticatorLinker.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Specialized; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SteamAuth 9 | { 10 | /// 11 | /// Handles the linking process for a new mobile authenticator. 12 | /// 13 | public class AuthenticatorLinker 14 | { 15 | /// 16 | /// Session data containing an access token for a steam account generated with k_EAuthTokenPlatformType_MobileApp 17 | /// 18 | private SessionData Session = null; 19 | 20 | /// 21 | /// Set to register a new phone number when linking. If a phone number is not set on the account, this must be set. If a phone number is set on the account, this must be null. 22 | /// 23 | public string PhoneNumber = null; 24 | public string PhoneCountryCode = null; 25 | public string PhoneSMSCode = null; 26 | 27 | /// 28 | /// Randomly-generated device ID. Should only be generated once per linker. 29 | /// 30 | public string DeviceID { get; private set; } 31 | 32 | /// 33 | /// After the initial link step, if successful, this will be the SteamGuard data for the account. PLEASE save this somewhere after generating it; it's vital data. 34 | /// 35 | public SteamGuardAccount LinkedAccount { get; private set; } 36 | 37 | /// 38 | /// True if the authenticator has been fully finalized. 39 | /// 40 | public bool Finalized = false; 41 | 42 | /// 43 | /// Email address the confirmation email was sent to when adding a phone number 44 | /// 45 | public string ConfirmationEmailAddress; 46 | 47 | /// 48 | /// Current step of AddPhoneNumber() 49 | /// 50 | private PhoneLinkStep phoneLinkStep; 51 | 52 | /// 53 | /// Create a new instance of AuthenticatorLinker 54 | /// 55 | /// Access token for a Steam account created with k_EAuthTokenPlatformType_MobileApp 56 | /// 64 bit formatted steamid for the account 57 | public AuthenticatorLinker(SessionData sessionData) 58 | { 59 | this.Session = sessionData; 60 | this.DeviceID = GenerateDeviceID(); 61 | } 62 | 63 | public async Task AddPhoneNumber() 64 | { 65 | if (this.phoneLinkStep == PhoneLinkStep.None) 66 | { 67 | // Check if the account has a phone number on it 68 | var accountPhoneStatus = await _getAccountPhoneStatus(); 69 | 70 | if (accountPhoneStatus.Response.VerifiedPhone) 71 | return PhoneLinkResult.PhoneAdded; 72 | 73 | if (string.IsNullOrEmpty(this.PhoneNumber)) 74 | return PhoneLinkResult.MustProvidePhoneNumber; 75 | 76 | // No phone verified, add one 77 | 78 | // Get country code 79 | string countryCode = this.PhoneCountryCode; 80 | 81 | // If given country code is null, use the one from the Steam account 82 | if (string.IsNullOrEmpty(countryCode)) 83 | countryCode = await getUserCountry(); 84 | 85 | // Set the phone number 86 | var setPhoneResponse = await _setAccountPhoneNumber(this.PhoneNumber, countryCode); 87 | 88 | // Make sure it's successful then respond that we must confirm via email 89 | if (setPhoneResponse != null && setPhoneResponse.Response.ConfirmationEmailAddress != null) 90 | { 91 | this.ConfirmationEmailAddress = setPhoneResponse.Response.ConfirmationEmailAddress; 92 | this.phoneLinkStep = PhoneLinkStep.ConfirmationEmailSent; 93 | return PhoneLinkResult.MustConfirmEmail; 94 | } 95 | } 96 | else if (this.phoneLinkStep == PhoneLinkStep.ConfirmationEmailSent) 97 | { 98 | // We are at past the "_setAccountPhoneNumber" step 99 | 100 | // Make sure the email was confirmed 101 | bool isStillWaiting = await _isAccountWaitingForEmailConfirmation(); 102 | if (isStillWaiting) 103 | return PhoneLinkResult.MustConfirmEmail; 104 | 105 | // Now send the SMS to the phone number 106 | await _sendPhoneVerificationCode(); 107 | 108 | // This takes time so wait a bit 109 | await Task.Delay(2000); 110 | 111 | this.phoneLinkStep = PhoneLinkStep.SMSCodeSent; 112 | return PhoneLinkResult.MustConfirmSMS; 113 | } 114 | else if (this.phoneLinkStep == PhoneLinkStep.SMSCodeSent) 115 | { 116 | // Make sure PhoneSMSCode is provided 117 | if (this.PhoneSMSCode == null) 118 | return PhoneLinkResult.MustConfirmSMS; 119 | 120 | var verifyResponse = await _verifyPhoneWithCode(this.PhoneSMSCode); 121 | 122 | // TODO: What happens when it fails? 123 | 124 | return PhoneLinkResult.PhoneAdded; 125 | } 126 | 127 | // If something else fails, we end up here 128 | return PhoneLinkResult.FailureAddingPhone; 129 | } 130 | 131 | /// 132 | /// First step in adding a mobile authenticator to an account 133 | /// 134 | public async Task AddAuthenticator() 135 | { 136 | 137 | // Make request to ITwoFactorService/AddAuthenticator 138 | NameValueCollection addAuthenticatorBody = new NameValueCollection(); 139 | addAuthenticatorBody.Add("steamid", this.Session.SteamID.ToString()); 140 | addAuthenticatorBody.Add("authenticator_type", "1"); 141 | addAuthenticatorBody.Add("device_identifier", this.DeviceID); 142 | addAuthenticatorBody.Add("sms_phone_id", "1"); 143 | addAuthenticatorBody.Add("version", "2"); 144 | string addAuthenticatorResponseStr = await SteamWeb.POSTRequest("https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/?access_token=" + this.Session.AccessToken, null, addAuthenticatorBody); 145 | 146 | // Parse response json to object 147 | var addAuthenticatorResponse = JsonConvert.DeserializeObject(addAuthenticatorResponseStr); 148 | 149 | if (addAuthenticatorResponse == null || addAuthenticatorResponse.Response == null) 150 | return LinkResult.GeneralFailure; 151 | 152 | // Status 2 means no phone number is on the account 153 | if (addAuthenticatorResponse.Response.Status == 2) 154 | return LinkResult.MustProvidePhoneNumber; 155 | 156 | if (addAuthenticatorResponse.Response.Status == 29) 157 | return LinkResult.AuthenticatorPresent; 158 | 159 | if (addAuthenticatorResponse.Response.Status != 1) 160 | return LinkResult.GeneralFailure; 161 | 162 | // Setup this.LinkedAccount 163 | this.LinkedAccount = addAuthenticatorResponse.Response; 164 | this.LinkedAccount.DeviceID = this.DeviceID; 165 | this.LinkedAccount.Session = this.Session; 166 | 167 | return LinkResult.AwaitingFinalization; 168 | } 169 | 170 | public async Task FinalizeAddAuthenticator(string smsCode) 171 | { 172 | int tries = 0; 173 | while (tries <= 10) 174 | { 175 | NameValueCollection finalizeAuthenticatorValues = new NameValueCollection(); 176 | finalizeAuthenticatorValues.Add("steamid", this.Session.SteamID.ToString()); 177 | finalizeAuthenticatorValues.Add("authenticator_code", LinkedAccount.GenerateSteamGuardCode()); 178 | finalizeAuthenticatorValues.Add("authenticator_time", TimeAligner.GetSteamTime().ToString()); 179 | finalizeAuthenticatorValues.Add("activation_code", smsCode); 180 | finalizeAuthenticatorValues.Add("validate_sms_code", "1"); 181 | 182 | string finalizeAuthenticatorResultStr; 183 | using (WebClient wc = new WebClient()) 184 | { 185 | wc.Encoding = Encoding.UTF8; 186 | wc.Headers[HttpRequestHeader.UserAgent] = SteamWeb.MOBILE_APP_USER_AGENT; 187 | byte[] finalizeAuthenticatorResult = await wc.UploadValuesTaskAsync(new Uri("https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=" + this.Session.AccessToken), "POST", finalizeAuthenticatorValues); 188 | finalizeAuthenticatorResultStr = Encoding.UTF8.GetString(finalizeAuthenticatorResult); 189 | } 190 | 191 | FinalizeAuthenticatorResponse finalizeAuthenticatorResponse = JsonConvert.DeserializeObject(finalizeAuthenticatorResultStr); 192 | 193 | if (finalizeAuthenticatorResponse == null || finalizeAuthenticatorResponse.Response == null) 194 | { 195 | return FinalizeResult.GeneralFailure; 196 | } 197 | 198 | if (finalizeAuthenticatorResponse.Response.Status == 89) 199 | { 200 | return FinalizeResult.BadSMSCode; 201 | } 202 | 203 | if (finalizeAuthenticatorResponse.Response.Status == 88) 204 | { 205 | if (tries >= 10) 206 | { 207 | return FinalizeResult.UnableToGenerateCorrectCodes; 208 | } 209 | } 210 | 211 | if (!finalizeAuthenticatorResponse.Response.Success) 212 | { 213 | return FinalizeResult.GeneralFailure; 214 | } 215 | 216 | if (finalizeAuthenticatorResponse.Response.WantMore) 217 | { 218 | tries++; 219 | continue; 220 | } 221 | 222 | this.LinkedAccount.FullyEnrolled = true; 223 | return FinalizeResult.Success; 224 | } 225 | 226 | return FinalizeResult.GeneralFailure; 227 | } 228 | 229 | private async Task getUserCountry() 230 | { 231 | NameValueCollection getCountryBody = new NameValueCollection(); 232 | getCountryBody.Add("steamid", this.Session.SteamID.ToString()); 233 | string getCountryResponseStr = await SteamWeb.POSTRequest("https://api.steampowered.com/IUserAccountService/GetUserCountry/v1?access_token=" + this.Session.AccessToken, null, getCountryBody); 234 | 235 | // Parse response json to object 236 | GetUserCountryResponse response = JsonConvert.DeserializeObject(getCountryResponseStr); 237 | return response.Response.Country; 238 | } 239 | 240 | private async Task _getAccountPhoneStatus() 241 | { 242 | string getCountryResponseStr = await SteamWeb.POSTRequest("https://api.steampowered.com/IPhoneService/AccountPhoneStatus/v1?access_token=" + this.Session.AccessToken, null, null); 243 | return JsonConvert.DeserializeObject(getCountryResponseStr); 244 | } 245 | 246 | private async Task _setAccountPhoneNumber(string phoneNumber, string countryCode) 247 | { 248 | NameValueCollection setPhoneBody = new NameValueCollection(); 249 | setPhoneBody.Add("phone_number", phoneNumber); 250 | setPhoneBody.Add("phone_country_code", countryCode); 251 | string setPhoneResponseStr = await SteamWeb.POSTRequest("https://api.steampowered.com/IPhoneService/SetAccountPhoneNumber/v1?access_token=" + this.Session.AccessToken, null, setPhoneBody); 252 | return JsonConvert.DeserializeObject(setPhoneResponseStr); 253 | } 254 | 255 | private async Task _verifyPhoneWithCode(string code) 256 | { 257 | NameValueCollection verifyPhoneBody = new NameValueCollection(); 258 | verifyPhoneBody.Add("code", code); 259 | string verifyPhoneResponseStr = await SteamWeb.POSTRequest("https://api.steampowered.com/IPhoneService/VerifyAccountPhoneWithCode/v1/?access_token=" + this.Session.AccessToken, null, verifyPhoneBody); 260 | return JsonConvert.DeserializeObject(verifyPhoneResponseStr); 261 | } 262 | 263 | private async Task _isAccountWaitingForEmailConfirmation() 264 | { 265 | string waitingForEmailResponse = await SteamWeb.POSTRequest("https://api.steampowered.com/IPhoneService/IsAccountWaitingForEmailConfirmation/v1?access_token=" + this.Session.AccessToken, null, null); 266 | 267 | // Parse response json to object 268 | var response = JsonConvert.DeserializeObject(waitingForEmailResponse); 269 | return response.Response.AwaitingEmailConfirmation; 270 | } 271 | 272 | private async Task _sendPhoneVerificationCode() 273 | { 274 | await SteamWeb.POSTRequest("https://api.steampowered.com/IPhoneService/SendPhoneVerificationCode/v1?access_token=" + this.Session.AccessToken, null, null); 275 | return true; 276 | } 277 | 278 | public enum LinkResult 279 | { 280 | MustProvidePhoneNumber, //No phone number on the account 281 | MustRemovePhoneNumber, //A phone number is already on the account 282 | MustConfirmEmail, //User need to click link from confirmation email 283 | AwaitingFinalization, //Must provide an SMS code 284 | GeneralFailure, //General failure (really now!) 285 | AuthenticatorPresent, 286 | FailureAddingPhone 287 | } 288 | 289 | public enum PhoneLinkResult 290 | { 291 | MustProvidePhoneNumber, //No phone number on the account 292 | MustConfirmEmail, //User need to click link from confirmation email 293 | FailureAddingPhone, 294 | PhoneAdded, 295 | MustConfirmSMS 296 | } 297 | 298 | private enum PhoneLinkStep 299 | { 300 | None, 301 | ConfirmationEmailSent, 302 | SMSCodeSent 303 | } 304 | 305 | public enum FinalizeResult 306 | { 307 | BadSMSCode, 308 | UnableToGenerateCorrectCodes, 309 | Success, 310 | GeneralFailure 311 | } 312 | 313 | private class GetUserCountryResponse 314 | { 315 | [JsonProperty("response")] 316 | public GetUserCountryResponseResponse Response { get; set; } 317 | } 318 | 319 | private class GetUserCountryResponseResponse 320 | { 321 | [JsonProperty("country")] 322 | public string Country { get; set; } 323 | } 324 | 325 | private class AccountPhoneStatusResponse 326 | { 327 | [JsonProperty("response")] 328 | public AccountPhoneStatusResponseResponse Response { get; set; } 329 | } 330 | 331 | private class AccountPhoneStatusResponseResponse 332 | { 333 | [JsonProperty("verified_phone")] 334 | public bool VerifiedPhone { get; set; } 335 | } 336 | 337 | 338 | private class VerifyPhoneResponse 339 | { 340 | [JsonProperty("response")] 341 | public VerifyPhoneResponseResponse Response { get; set; } 342 | } 343 | 344 | private class VerifyPhoneResponseResponse 345 | { 346 | 347 | } 348 | 349 | private class SetAccountPhoneNumberResponse 350 | { 351 | [JsonProperty("response")] 352 | public SetAccountPhoneNumberResponseResponse Response { get; set; } 353 | } 354 | 355 | private class SetAccountPhoneNumberResponseResponse 356 | { 357 | [JsonProperty("confirmation_email_address")] 358 | public string ConfirmationEmailAddress { get; set; } 359 | 360 | [JsonProperty("phone_number_formatted")] 361 | public string PhoneNumberFormatted { get; set; } 362 | } 363 | 364 | private class IsAccountWaitingForEmailConfirmationResponse 365 | { 366 | [JsonProperty("response")] 367 | public IsAccountWaitingForEmailConfirmationResponseResponse Response { get; set; } 368 | } 369 | 370 | private class IsAccountWaitingForEmailConfirmationResponseResponse 371 | { 372 | [JsonProperty("awaiting_email_confirmation")] 373 | public bool AwaitingEmailConfirmation { get; set; } 374 | 375 | [JsonProperty("seconds_to_wait")] 376 | public int SecondsToWait { get; set; } 377 | } 378 | 379 | private class AddAuthenticatorResponse 380 | { 381 | [JsonProperty("response")] 382 | public SteamGuardAccount Response { get; set; } 383 | } 384 | 385 | private class FinalizeAuthenticatorResponse 386 | { 387 | [JsonProperty("response")] 388 | public FinalizeAuthenticatorInternalResponse Response { get; set; } 389 | 390 | internal class FinalizeAuthenticatorInternalResponse 391 | { 392 | [JsonProperty("success")] 393 | public bool Success { get; set; } 394 | 395 | [JsonProperty("want_more")] 396 | public bool WantMore { get; set; } 397 | 398 | [JsonProperty("server_time")] 399 | public long ServerTime { get; set; } 400 | 401 | [JsonProperty("status")] 402 | public int Status { get; set; } 403 | } 404 | } 405 | 406 | public static string GenerateDeviceID() 407 | { 408 | return "android:" + Guid.NewGuid().ToString(); 409 | } 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /SteamAuth/Confirmation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | 6 | namespace SteamAuth 7 | { 8 | public class Confirmation 9 | { 10 | [JsonProperty(PropertyName = "id")] 11 | public ulong ID { get; set; } 12 | 13 | [JsonProperty(PropertyName = "nonce")] 14 | public ulong Key { get; set; } 15 | 16 | [JsonProperty(PropertyName = "creator_id")] 17 | public ulong Creator { get; set; } 18 | 19 | [JsonProperty(PropertyName = "headline")] 20 | public string Headline { get; set; } 21 | 22 | [JsonProperty(PropertyName = "summary")] 23 | public List Summary { get; set; } 24 | 25 | [JsonProperty(PropertyName = "accept")] 26 | public string Accept { get; set; } 27 | 28 | [JsonProperty(PropertyName = "cancel")] 29 | public string Cancel { get; set; } 30 | 31 | [JsonProperty(PropertyName = "icon")] 32 | public string Icon { get; set; } 33 | 34 | [JsonProperty(PropertyName = "type")] 35 | [JsonConverter(typeof(StringEnumConverter))] 36 | public EMobileConfirmationType ConfType { get; set; } = EMobileConfirmationType.Invalid; 37 | 38 | public enum EMobileConfirmationType 39 | { 40 | Invalid = 0, 41 | Test = 1, 42 | Trade = 2, 43 | MarketListing = 3, 44 | FeatureOptOut = 4, 45 | PhoneNumberChange = 5, 46 | AccountRecovery = 6 47 | } 48 | } 49 | 50 | public class ConfirmationsResponse 51 | { 52 | [JsonProperty("success")] 53 | public bool Success { get; set; } 54 | 55 | [JsonProperty("message")] 56 | public string Message { get; set; } 57 | 58 | [JsonProperty("needauth")] 59 | public bool NeedAuthentication { get; set; } 60 | 61 | [JsonProperty("conf")] 62 | public Confirmation[] Confirmations { get; set; } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SteamAuth/CookieAwareWebClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace SteamAuth 5 | { 6 | public class CookieAwareWebClient : WebClient 7 | { 8 | public CookieContainer CookieContainer { get; set; } = new CookieContainer(); 9 | public CookieCollection ResponseCookies { get; set; } = new CookieCollection(); 10 | 11 | protected override WebRequest GetWebRequest(Uri address) 12 | { 13 | var request = (HttpWebRequest)base.GetWebRequest(address); 14 | request.CookieContainer = CookieContainer; 15 | return request; 16 | } 17 | 18 | protected override WebResponse GetWebResponse(WebRequest request) 19 | { 20 | var response = (HttpWebResponse)base.GetWebResponse(request); 21 | this.ResponseCookies = response.Cookies; 22 | return response; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SteamAuth/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Setting ComVisible to false makes the types in this assembly not visible 6 | // to COM components. If you need to access a type in this assembly from 7 | // COM, set the ComVisible attribute to true on that type. 8 | [assembly: ComVisible(false)] 9 | 10 | // The following GUID is for the ID of the typelib if this project is exposed to COM 11 | [assembly: Guid("5ad0934e-f6c4-4ae5-83af-c788313b2a87")] 12 | -------------------------------------------------------------------------------- /SteamAuth/SessionData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Runtime.CompilerServices; 7 | using System.Threading.Tasks; 8 | 9 | namespace SteamAuth 10 | { 11 | public class SessionData 12 | { 13 | public ulong SteamID { get; set; } 14 | 15 | public string AccessToken { get; set; } 16 | 17 | public string RefreshToken { get; set; } 18 | 19 | public string SessionID { get; set; } 20 | 21 | /// 22 | /// Refresh your access token, optionally also getting a new refresh token 23 | /// 24 | /// Allow getting a new refresh token as well. If one is returned, this.RefreshToken will be overwritten. You must save this new token! 25 | /// 26 | /// 27 | public async Task RefreshAccessToken(bool allowRenewal = false) 28 | { 29 | if (string.IsNullOrEmpty(this.RefreshToken)) 30 | throw new Exception("Refresh token is empty"); 31 | 32 | if (IsTokenExpired(this.RefreshToken)) 33 | throw new Exception("Refresh token is expired"); 34 | 35 | string responseStr; 36 | try 37 | { 38 | var postData = new NameValueCollection(); 39 | postData.Add("refresh_token", this.RefreshToken); 40 | postData.Add("steamid", this.SteamID.ToString()); 41 | postData.Add("renewal_type", allowRenewal ? "1" : "0"); 42 | responseStr = await SteamWeb.POSTRequest("https://api.steampowered.com/IAuthenticationService/GenerateAccessTokenForApp/v1/", null, postData); 43 | } 44 | catch (Exception ex) 45 | { 46 | throw new Exception("Failed to refresh token: " + ex.Message); 47 | } 48 | 49 | var response = JsonConvert.DeserializeObject(responseStr); 50 | this.AccessToken = response.Response.AccessToken; 51 | 52 | if (!string.IsNullOrEmpty(response.Response.RefreshToken)) 53 | this.RefreshToken = response.Response.RefreshToken; 54 | } 55 | 56 | public bool IsAccessTokenExpired() 57 | { 58 | if (string.IsNullOrEmpty(this.AccessToken)) 59 | return true; 60 | 61 | return IsTokenExpired(this.AccessToken); 62 | } 63 | 64 | public bool IsRefreshTokenExpired() 65 | { 66 | if (string.IsNullOrEmpty(this.RefreshToken)) 67 | return true; 68 | 69 | return IsTokenExpired(this.RefreshToken); 70 | } 71 | 72 | private bool IsTokenExpired(string token) 73 | { 74 | // Compare expire time of the token to the current time 75 | return DateTimeOffset.UtcNow.ToUnixTimeSeconds() > GetTokenExpirationTime(token); 76 | } 77 | 78 | /// 79 | /// If the token is going to expire within the next 24h. 80 | /// 81 | /// 82 | public bool IsRefreshTokenAboutToExpire() 83 | { 84 | return IsRefreshTokenExpired() || IsTokenAboutToExpire(RefreshToken); 85 | } 86 | 87 | /// 88 | /// Returns if the token will expire 89 | /// 90 | /// 91 | /// 92 | private bool IsTokenAboutToExpire(string token) 93 | { 94 | // Compare expire time of the token to the current time 95 | return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (24 * 60 * 60) > GetTokenExpirationTime(token); 96 | } 97 | 98 | /// 99 | /// Fetches JWT expiration time. 100 | /// 101 | /// 102 | /// 103 | private long GetTokenExpirationTime(string token) 104 | { 105 | string[] tokenComponents = token.Split('.'); 106 | // Fix up base64url to normal base64 107 | string base64 = tokenComponents[1].Replace('-', '+').Replace('_', '/'); 108 | 109 | if (base64.Length % 4 != 0) 110 | { 111 | base64 += new string('=', 4 - base64.Length % 4); 112 | } 113 | 114 | byte[] payloadBytes = Convert.FromBase64String(base64); 115 | SteamAccessToken jwt = JsonConvert.DeserializeObject(System.Text.Encoding.UTF8.GetString(payloadBytes)); 116 | 117 | return jwt.exp; 118 | } 119 | 120 | public CookieContainer GetCookies() 121 | { 122 | if (this.SessionID == null) 123 | this.SessionID = GenerateSessionID(); 124 | 125 | var cookies = new CookieContainer(); 126 | foreach (string domain in new string[] { "steamcommunity.com", "store.steampowered.com" }) 127 | { 128 | cookies.Add(new Cookie("steamLoginSecure", this.GetSteamLoginSecure(), "/", domain)); 129 | cookies.Add(new Cookie("sessionid", this.SessionID, "/", domain)); 130 | cookies.Add(new Cookie("mobileClient", "android", "/", domain)); 131 | cookies.Add(new Cookie("mobileClientVersion", "777777 3.6.4", "/", domain)); 132 | } 133 | return cookies; 134 | } 135 | 136 | private string GetSteamLoginSecure() 137 | { 138 | return this.SteamID.ToString() + "%7C%7C" + this.AccessToken; 139 | } 140 | 141 | private static string GenerateSessionID() 142 | { 143 | return GetRandomHexNumber(32); 144 | } 145 | 146 | private static string GetRandomHexNumber(int digits) 147 | { 148 | Random random = new Random(); 149 | byte[] buffer = new byte[digits / 2]; 150 | random.NextBytes(buffer); 151 | string result = String.Concat(buffer.Select(x => x.ToString("X2")).ToArray()); 152 | if (digits % 2 == 0) 153 | return result; 154 | return result + random.Next(16).ToString("X"); 155 | } 156 | 157 | private class SteamAccessToken 158 | { 159 | public long exp { get; set; } 160 | } 161 | 162 | private class GenerateAccessTokenForAppResponse 163 | { 164 | [JsonProperty("response")] 165 | public GenerateAccessTokenForAppResponseResponse Response; 166 | } 167 | 168 | private class GenerateAccessTokenForAppResponseResponse 169 | { 170 | [JsonProperty("access_token")] 171 | public string AccessToken { get; set; } 172 | 173 | [JsonProperty("refresh_token")] 174 | public string RefreshToken { get; set; } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SteamAuth/SteamAuth.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | {5AD0934E-F6C4-4AE5-83AF-C788313B2A87} 4 | netstandard2.0 5 | SteamAuth 6 | SteamAuth 7 | Copyright © 2015 8 | bin\$(Configuration)\ 9 | 10 | 11 | full 12 | 13 | 14 | pdbonly 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SteamAuth/SteamAuth.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBed", "..\TestBed\TestBed.csproj", "{8A732227-C090-4011-9F0A-51180CFE6271}" 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 | {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /SteamAuth/SteamGuardAccount.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | 11 | namespace SteamAuth 12 | { 13 | public class SteamGuardAccount 14 | { 15 | [JsonProperty("shared_secret")] 16 | public string SharedSecret { get; set; } 17 | 18 | [JsonProperty("serial_number")] 19 | public string SerialNumber { get; set; } 20 | 21 | [JsonProperty("revocation_code")] 22 | public string RevocationCode { get; set; } 23 | 24 | [JsonProperty("uri")] 25 | public string URI { get; set; } 26 | 27 | [JsonProperty("server_time")] 28 | public long ServerTime { get; set; } 29 | 30 | [JsonProperty("account_name")] 31 | public string AccountName { get; set; } 32 | 33 | [JsonProperty("token_gid")] 34 | public string TokenGID { get; set; } 35 | 36 | [JsonProperty("identity_secret")] 37 | public string IdentitySecret { get; set; } 38 | 39 | [JsonProperty("secret_1")] 40 | public string Secret1 { get; set; } 41 | 42 | [JsonProperty("status")] 43 | public int Status { get; set; } 44 | 45 | // Deprecated? 46 | [JsonProperty("device_id")] 47 | public string DeviceID { get; set; } 48 | 49 | [JsonProperty("phone_number_hint")] 50 | public string PhoneNumberHint { get; set; } 51 | 52 | [JsonProperty("confirm_type")] 53 | public int ConfirmType { get; set; } 54 | 55 | /// 56 | /// Set to true if the authenticator has actually been applied to the account. 57 | /// 58 | [JsonProperty("fully_enrolled")] 59 | public bool FullyEnrolled { get; set; } 60 | 61 | public SessionData Session { get; set; } 62 | 63 | private static byte[] steamGuardCodeTranslations = new byte[] { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 }; 64 | 65 | /// 66 | /// Remove steam guard from this account 67 | /// 68 | /// 1 = Return to email codes, 2 = Remove completley 69 | /// 70 | public async Task DeactivateAuthenticator(int scheme = 1) 71 | { 72 | var postBody = new NameValueCollection(); 73 | postBody.Add("revocation_code", this.RevocationCode); 74 | postBody.Add("revocation_reason", "1"); 75 | postBody.Add("steamguard_scheme", scheme.ToString()); 76 | string response = await SteamWeb.POSTRequest("https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1?access_token=" + this.Session.AccessToken, null, postBody); 77 | 78 | // Parse to object 79 | var removeResponse = JsonConvert.DeserializeObject(response); 80 | 81 | if (removeResponse == null || removeResponse.Response == null || !removeResponse.Response.Success) return false; 82 | return true; 83 | } 84 | 85 | public string GenerateSteamGuardCode() 86 | { 87 | return GenerateSteamGuardCodeForTime(TimeAligner.GetSteamTime()); 88 | } 89 | 90 | public async Task GenerateSteamGuardCodeAsync() 91 | { 92 | return GenerateSteamGuardCodeForTime(await TimeAligner.GetSteamTimeAsync()); 93 | } 94 | 95 | public string GenerateSteamGuardCodeForTime(long time) 96 | { 97 | if (this.SharedSecret == null || this.SharedSecret.Length == 0) 98 | { 99 | return ""; 100 | } 101 | 102 | string sharedSecretUnescaped = Regex.Unescape(this.SharedSecret); 103 | byte[] sharedSecretArray = Convert.FromBase64String(sharedSecretUnescaped); 104 | byte[] timeArray = new byte[8]; 105 | 106 | time /= 30L; 107 | 108 | for (int i = 8; i > 0; i--) 109 | { 110 | timeArray[i - 1] = (byte)time; 111 | time >>= 8; 112 | } 113 | 114 | HMACSHA1 hmacGenerator = new HMACSHA1(); 115 | hmacGenerator.Key = sharedSecretArray; 116 | byte[] hashedData = hmacGenerator.ComputeHash(timeArray); 117 | byte[] codeArray = new byte[5]; 118 | try 119 | { 120 | byte b = (byte)(hashedData[19] & 0xF); 121 | int codePoint = (hashedData[b] & 0x7F) << 24 | (hashedData[b + 1] & 0xFF) << 16 | (hashedData[b + 2] & 0xFF) << 8 | (hashedData[b + 3] & 0xFF); 122 | 123 | for (int i = 0; i < 5; ++i) 124 | { 125 | codeArray[i] = steamGuardCodeTranslations[codePoint % steamGuardCodeTranslations.Length]; 126 | codePoint /= steamGuardCodeTranslations.Length; 127 | } 128 | } 129 | catch (Exception) 130 | { 131 | return null; //Change later, catch-alls are bad! 132 | } 133 | return Encoding.UTF8.GetString(codeArray); 134 | } 135 | 136 | public Confirmation[] FetchConfirmations() 137 | { 138 | string url = this.GenerateConfirmationURL(); 139 | string response = SteamWeb.GETRequest(url, this.Session.GetCookies()).Result; 140 | return FetchConfirmationInternal(response); 141 | } 142 | 143 | public async Task FetchConfirmationsAsync() 144 | { 145 | string url = this.GenerateConfirmationURL(); 146 | string response = await SteamWeb.GETRequest(url, this.Session.GetCookies()); 147 | return FetchConfirmationInternal(response); 148 | } 149 | 150 | private Confirmation[] FetchConfirmationInternal(string response) 151 | { 152 | var confirmationsResponse = JsonConvert.DeserializeObject(response); 153 | 154 | if (confirmationsResponse == null || !confirmationsResponse.Success) 155 | { 156 | throw new Exception(confirmationsResponse.Message); 157 | } 158 | 159 | if (confirmationsResponse.NeedAuthentication) 160 | { 161 | throw new Exception("Needs Authentication"); 162 | } 163 | 164 | return confirmationsResponse.Confirmations; 165 | } 166 | 167 | /// 168 | /// Deprecated. Simply returns conf.Creator. 169 | /// 170 | /// 171 | /// The Creator field of conf 172 | public long GetConfirmationTradeOfferID(Confirmation conf) 173 | { 174 | if (conf.ConfType != Confirmation.EMobileConfirmationType.Trade) 175 | throw new ArgumentException("conf must be a trade confirmation."); 176 | 177 | return (long)conf.Creator; 178 | } 179 | 180 | public async Task AcceptMultipleConfirmations(Confirmation[] confs) 181 | { 182 | return await _sendMultiConfirmationAjax(confs, "allow"); 183 | } 184 | 185 | public async Task DenyMultipleConfirmations(Confirmation[] confs) 186 | { 187 | return await _sendMultiConfirmationAjax(confs, "cancel"); 188 | } 189 | 190 | public async Task AcceptConfirmation(Confirmation conf) 191 | { 192 | return await _sendConfirmationAjax(conf, "allow"); 193 | } 194 | 195 | public async Task DenyConfirmation(Confirmation conf) 196 | { 197 | return await _sendConfirmationAjax(conf, "cancel"); 198 | } 199 | 200 | private async Task _sendConfirmationAjax(Confirmation conf, string op) 201 | { 202 | string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/ajaxop"; 203 | string queryString = "?op=" + op + "&"; 204 | // tag is different from op now 205 | string tag = op == "allow" ? "accept" : "reject"; 206 | queryString += GenerateConfirmationQueryParams(tag); 207 | queryString += "&cid=" + conf.ID + "&ck=" + conf.Key; 208 | url += queryString; 209 | 210 | string response = await SteamWeb.GETRequest(url, this.Session.GetCookies()); 211 | if (response == null) return false; 212 | 213 | SendConfirmationResponse confResponse = JsonConvert.DeserializeObject(response); 214 | return confResponse.Success; 215 | } 216 | 217 | private async Task _sendMultiConfirmationAjax(Confirmation[] confs, string op) 218 | { 219 | string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/multiajaxop"; 220 | // tag is different from op now 221 | string tag = op == "allow" ? "accept" : "reject"; 222 | string query = "op=" + op + "&" + GenerateConfirmationQueryParams(tag); 223 | foreach (var conf in confs) 224 | { 225 | query += "&cid[]=" + conf.ID + "&ck[]=" + conf.Key; 226 | } 227 | 228 | string response; 229 | using (CookieAwareWebClient wc = new CookieAwareWebClient()) 230 | { 231 | wc.Encoding = Encoding.UTF8; 232 | wc.CookieContainer = this.Session.GetCookies(); 233 | wc.Headers["Origin"] = APIEndpoints.COMMUNITY_BASE; 234 | wc.Headers[HttpRequestHeader.UserAgent] = SteamWeb.MOBILE_APP_USER_AGENT; 235 | wc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded; charset=UTF-8"; 236 | response = await wc.UploadStringTaskAsync(new Uri(url), "POST", query); 237 | } 238 | if (response == null) return false; 239 | 240 | SendConfirmationResponse confResponse = JsonConvert.DeserializeObject(response); 241 | return confResponse.Success; 242 | } 243 | 244 | public string GenerateConfirmationURL(string tag = "conf") 245 | { 246 | string endpoint = APIEndpoints.COMMUNITY_BASE + "/mobileconf/getlist?"; 247 | string queryString = GenerateConfirmationQueryParams(tag); 248 | return endpoint + queryString; 249 | } 250 | 251 | public string GenerateConfirmationQueryParams(string tag) 252 | { 253 | if (String.IsNullOrEmpty(DeviceID)) 254 | throw new ArgumentException("Device ID is not present"); 255 | 256 | var queryParams = GenerateConfirmationQueryParamsAsNVC(tag); 257 | 258 | return string.Join("&", queryParams.AllKeys.Select(key => $"{key}={queryParams[key]}")); 259 | } 260 | 261 | public NameValueCollection GenerateConfirmationQueryParamsAsNVC(string tag) 262 | { 263 | if (String.IsNullOrEmpty(DeviceID)) 264 | throw new ArgumentException("Device ID is not present"); 265 | 266 | long time = TimeAligner.GetSteamTime(); 267 | 268 | var ret = new NameValueCollection(); 269 | ret.Add("p", this.DeviceID); 270 | ret.Add("a", this.Session.SteamID.ToString()); 271 | ret.Add("k", _generateConfirmationHashForTime(time, tag)); 272 | ret.Add("t", time.ToString()); 273 | ret.Add("m", "react"); 274 | ret.Add("tag", tag); 275 | 276 | return ret; 277 | } 278 | 279 | private string _generateConfirmationHashForTime(long time, string tag) 280 | { 281 | byte[] decode = Convert.FromBase64String(this.IdentitySecret); 282 | int n2 = 8; 283 | if (tag != null) 284 | { 285 | if (tag.Length > 32) 286 | { 287 | n2 = 8 + 32; 288 | } 289 | else 290 | { 291 | n2 = 8 + tag.Length; 292 | } 293 | } 294 | byte[] array = new byte[n2]; 295 | int n3 = 8; 296 | while (true) 297 | { 298 | int n4 = n3 - 1; 299 | if (n3 <= 0) 300 | { 301 | break; 302 | } 303 | array[n4] = (byte)time; 304 | time >>= 8; 305 | n3 = n4; 306 | } 307 | if (tag != null) 308 | { 309 | Array.Copy(Encoding.UTF8.GetBytes(tag), 0, array, 8, n2 - 8); 310 | } 311 | 312 | try 313 | { 314 | HMACSHA1 hmacGenerator = new HMACSHA1(); 315 | hmacGenerator.Key = decode; 316 | byte[] hashedData = hmacGenerator.ComputeHash(array); 317 | string encodedData = Convert.ToBase64String(hashedData, Base64FormattingOptions.None); 318 | string hash = WebUtility.UrlEncode(encodedData); 319 | return hash; 320 | } 321 | catch 322 | { 323 | return null; 324 | } 325 | } 326 | 327 | public class WGTokenInvalidException : Exception 328 | { 329 | } 330 | 331 | public class WGTokenExpiredException : Exception 332 | { 333 | } 334 | 335 | private class RemoveAuthenticatorResponse 336 | { 337 | [JsonProperty("response")] 338 | public RemoveAuthenticatorInternalResponse Response { get; set; } 339 | 340 | internal class RemoveAuthenticatorInternalResponse 341 | { 342 | [JsonProperty("success")] 343 | public bool Success { get; set; } 344 | 345 | [JsonProperty("revocation_attempts_remaining")] 346 | public int RevocationAttemptsRemaining { get; set; } 347 | } 348 | } 349 | 350 | private class SendConfirmationResponse 351 | { 352 | [JsonProperty("success")] 353 | public bool Success { get; set; } 354 | } 355 | 356 | private class ConfirmationDetailsResponse 357 | { 358 | [JsonProperty("success")] 359 | public bool Success { get; set; } 360 | 361 | [JsonProperty("html")] 362 | public string HTML { get; set; } 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /SteamAuth/SteamWeb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Net; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SteamAuth 8 | { 9 | public class SteamWeb 10 | { 11 | public static string MOBILE_APP_USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 9; Valve Steam App Version/3)"; 12 | 13 | public static async Task GETRequest(string url, CookieContainer cookies) 14 | { 15 | string response; 16 | using (CookieAwareWebClient wc = new CookieAwareWebClient()) 17 | { 18 | wc.Encoding = Encoding.UTF8; 19 | wc.CookieContainer = cookies; 20 | wc.Headers[HttpRequestHeader.UserAgent] = SteamWeb.MOBILE_APP_USER_AGENT; 21 | response = await wc.DownloadStringTaskAsync(url); 22 | } 23 | return response; 24 | } 25 | 26 | public static async Task POSTRequest(string url, CookieContainer cookies, NameValueCollection body) 27 | { 28 | if (body == null) 29 | body = new NameValueCollection(); 30 | 31 | string response; 32 | using (CookieAwareWebClient wc = new CookieAwareWebClient()) 33 | { 34 | wc.Encoding = Encoding.UTF8; 35 | wc.CookieContainer = cookies; 36 | wc.Headers[HttpRequestHeader.UserAgent] = SteamWeb.MOBILE_APP_USER_AGENT; 37 | byte[] result = await wc.UploadValuesTaskAsync(new Uri(url), "POST", body); 38 | response = Encoding.UTF8.GetString(result); 39 | } 40 | return response; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SteamAuth/TimeAligner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Net; 4 | using Newtonsoft.Json; 5 | using System.Text; 6 | 7 | namespace SteamAuth 8 | { 9 | /// 10 | /// Class to help align system time with the Steam server time. Not super advanced; probably not taking some things into account that it should. 11 | /// Necessary to generate up-to-date codes. In general, this will have an error of less than a second, assuming Steam is operational. 12 | /// 13 | public class TimeAligner 14 | { 15 | private static bool _aligned = false; 16 | private static int _timeDifference = 0; 17 | 18 | public static long GetSteamTime() 19 | { 20 | if (!TimeAligner._aligned) 21 | { 22 | TimeAligner.AlignTime(); 23 | } 24 | return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + _timeDifference; 25 | } 26 | 27 | public static async Task GetSteamTimeAsync() 28 | { 29 | if (!TimeAligner._aligned) 30 | { 31 | await TimeAligner.AlignTimeAsync(); 32 | } 33 | return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + _timeDifference; 34 | } 35 | 36 | public static void AlignTime() 37 | { 38 | long currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 39 | using (WebClient client = new WebClient()) 40 | { 41 | client.Encoding = Encoding.UTF8; 42 | try 43 | { 44 | string response = client.UploadString(APIEndpoints.TWO_FACTOR_TIME_QUERY, "steamid=0"); 45 | TimeQuery query = JsonConvert.DeserializeObject(response); 46 | TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime); 47 | TimeAligner._aligned = true; 48 | } 49 | catch (WebException) 50 | { 51 | return; 52 | } 53 | } 54 | } 55 | 56 | public static async Task AlignTimeAsync() 57 | { 58 | long currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 59 | WebClient client = new WebClient(); 60 | try 61 | { 62 | client.Encoding = Encoding.UTF8; 63 | string response = await client.UploadStringTaskAsync(new Uri(APIEndpoints.TWO_FACTOR_TIME_QUERY), "steamid=0"); 64 | TimeQuery query = JsonConvert.DeserializeObject(response); 65 | TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime); 66 | TimeAligner._aligned = true; 67 | } 68 | catch (WebException) 69 | { 70 | return; 71 | } 72 | } 73 | 74 | internal class TimeQuery 75 | { 76 | [JsonProperty("response")] 77 | internal TimeQueryResponse Response { get; set; } 78 | 79 | internal class TimeQueryResponse 80 | { 81 | [JsonProperty("server_time")] 82 | public long ServerTime { get; set; } 83 | } 84 | 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /TestBed/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TestBed/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SteamAuth; 3 | using Newtonsoft.Json; 4 | using System.IO; 5 | using SteamKit2; 6 | using SteamKit2.Authentication; 7 | using SteamKit2.Internal; 8 | using System.Threading.Tasks; 9 | 10 | namespace TestBed 11 | { 12 | class Program 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | //This basic loop will log into user accounts you specify, enable the mobile authenticator, and save a maFile (mobile authenticator file) 17 | while (true) 18 | { 19 | // Start a new SteamClient instance 20 | SteamClient steamClient = new SteamClient(); 21 | 22 | // Connect to Steam 23 | steamClient.Connect(); 24 | 25 | // Really basic way to wait until Steam is connected 26 | while (!steamClient.IsConnected) 27 | await Task.Delay(500); 28 | 29 | Console.WriteLine("Enter username: "); 30 | string username = Console.ReadLine(); 31 | 32 | Console.WriteLine("Enter password: "); 33 | string password = Console.ReadLine(); 34 | 35 | // Create a new auth session 36 | CredentialsAuthSession authSession; 37 | try 38 | { 39 | authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails 40 | { 41 | Username = username, 42 | Password = password, 43 | IsPersistentSession = false, 44 | PlatformType = EAuthTokenPlatformType.k_EAuthTokenPlatformType_MobileApp, 45 | ClientOSType = EOSType.Android9, 46 | Authenticator = new UserConsoleAuthenticator(), 47 | }); 48 | } 49 | catch (Exception ex) 50 | { 51 | Console.WriteLine("Error logging in: " + ex.Message); 52 | return; 53 | } 54 | 55 | // Starting polling Steam for authentication response 56 | var pollResponse = await authSession.PollingWaitForResultAsync(); 57 | 58 | // Build a SessionData object 59 | SessionData sessionData = new SessionData() 60 | { 61 | SteamID = authSession.SteamID.ConvertToUInt64(), 62 | AccessToken = pollResponse.AccessToken, 63 | RefreshToken = pollResponse.RefreshToken, 64 | }; 65 | 66 | // Init AuthenticatorLinker 67 | AuthenticatorLinker linker = new AuthenticatorLinker(sessionData); 68 | 69 | Console.WriteLine("If account has no phone number, enter one now: (+1 XXXXXXXXXX)"); 70 | string phoneNumber = Console.ReadLine(); 71 | 72 | if (string.IsNullOrWhiteSpace(phoneNumber)) 73 | { 74 | linker.PhoneNumber = null; 75 | } 76 | else 77 | { 78 | linker.PhoneNumber = phoneNumber; 79 | } 80 | 81 | int tries = 0; 82 | AuthenticatorLinker.LinkResult result = AuthenticatorLinker.LinkResult.GeneralFailure; 83 | while (tries <= 5) 84 | { 85 | tries++; 86 | 87 | // Add authenticator 88 | result = await linker.AddAuthenticator(); 89 | 90 | if (result == AuthenticatorLinker.LinkResult.MustConfirmEmail) 91 | { 92 | Console.WriteLine("Click the link sent to your email address: " + linker.ConfirmationEmailAddress); 93 | Console.WriteLine("Press enter when done"); 94 | Console.ReadLine(); 95 | continue; 96 | } 97 | 98 | if (result == AuthenticatorLinker.LinkResult.MustProvidePhoneNumber) 99 | { 100 | Console.WriteLine("Account requires a phone number. Login again and enter one."); 101 | break; 102 | } 103 | 104 | if (result == AuthenticatorLinker.LinkResult.AuthenticatorPresent) 105 | { 106 | Console.WriteLine("Account already has an authenticator linked."); 107 | break; 108 | } 109 | 110 | if (result != AuthenticatorLinker.LinkResult.AwaitingFinalization) 111 | { 112 | Console.WriteLine("Failed to add authenticator: " + result); 113 | break; 114 | } 115 | 116 | // Write maFile 117 | try 118 | { 119 | string sgFile = JsonConvert.SerializeObject(linker.LinkedAccount, Formatting.Indented); 120 | string fileName = linker.LinkedAccount.AccountName + ".maFile"; 121 | File.WriteAllText(fileName, sgFile); 122 | break; 123 | } 124 | catch (Exception e) 125 | { 126 | Console.WriteLine(e.Message); 127 | Console.WriteLine("EXCEPTION saving maFile. For security, authenticator will not be finalized."); 128 | break; 129 | } 130 | } 131 | 132 | if (result != AuthenticatorLinker.LinkResult.AwaitingFinalization) 133 | continue; 134 | 135 | tries = 0; 136 | while (tries <= 5) 137 | { 138 | Console.WriteLine("Please enter SMS code: "); 139 | string smsCode = Console.ReadLine(); 140 | var linkResult = await linker.FinalizeAddAuthenticator(smsCode); 141 | 142 | if (linkResult != AuthenticatorLinker.FinalizeResult.Success) 143 | { 144 | Console.WriteLine("Failed to finalize authenticator: " + linkResult); 145 | continue; 146 | } 147 | 148 | Console.WriteLine("Authenticator finalized!"); 149 | break; 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /TestBed/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TestBed")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TestBed")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("8a732227-c090-4011-9f0a-51180cfe6271")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /TestBed/TestBed.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8A732227-C090-4011-9F0A-51180CFE6271} 8 | Exe 9 | Properties 10 | TestBed 11 | TestBed 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | ..\SteamAuth\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll 39 | 40 | 41 | ..\SteamAuth\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll 42 | 43 | 44 | ..\SteamAuth\packages\protobuf-net.3.2.16\lib\net462\protobuf-net.dll 45 | 46 | 47 | ..\SteamAuth\packages\protobuf-net.Core.3.2.16\lib\net462\protobuf-net.Core.dll 48 | 49 | 50 | ..\SteamAuth\packages\SteamKit2.2.5.0-Beta.1\lib\netstandard2.0\SteamKit2.dll 51 | 52 | 53 | 54 | ..\SteamAuth\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll 55 | 56 | 57 | ..\SteamAuth\packages\System.Collections.Immutable.7.0.0\lib\net462\System.Collections.Immutable.dll 58 | 59 | 60 | 61 | ..\SteamAuth\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll 62 | 63 | 64 | 65 | ..\SteamAuth\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 66 | 67 | 68 | ..\SteamAuth\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll 69 | 70 | 71 | ..\SteamAuth\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll 72 | 73 | 74 | ..\SteamAuth\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {5ad0934e-f6c4-4ae5-83af-c788313b2a87} 94 | SteamAuth 95 | 96 | 97 | 98 | 105 | -------------------------------------------------------------------------------- /TestBed/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------