source: TFP-WebServer/WebServer/src/OpenID.cs@ 442

Last change on this file since 442 was 402, checked in by alloc, 22 months ago
  • Major refactoring
  • Using Utf8Json for (de)serialization
  • Moving APIs to REST
  • Removing dependencies from WebServer and MapRenderer to ServerFixes
File size: 7.5 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.Collections.Specialized;
4using System.IO;
5using System.Net;
6using System.Net.Security;
7using System.Reflection;
8using System.Security.Cryptography.X509Certificates;
9using System.Text;
10using System.Text.RegularExpressions;
11using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest;
12
13namespace Webserver {
14 public static class OpenID {
15 private const string STEAM_LOGIN = "https://steamcommunity.com/openid/login";
16
17 private static readonly Regex steamIdUrlMatcher =
18 new Regex (@"^https?:\/\/steamcommunity\.com\/openid\/id\/([0-9]{17,18})");
19
20 private static readonly X509Certificate2 caCert =
21 new X509Certificate2 ($"{Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location)}/steam-rootca.cer");
22
23 private static readonly X509Certificate2 caIntermediateCert =
24 new X509Certificate2 ($"{Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location)}/steam-intermediate.cer");
25
26 private const bool verboseSsl = false;
27 public static bool debugOpenId;
28
29 static OpenID () {
30 for (int i = 0; i < Environment.GetCommandLineArgs ().Length; i++) {
31 if (Environment.GetCommandLineArgs () [i].EqualsCaseInsensitive ("-debugopenid")) {
32 debugOpenId = true;
33 }
34 }
35
36 ServicePointManager.ServerCertificateValidationCallback = (_srvPoint, _certificate, _chain, _errors) => {
37 if (_errors == SslPolicyErrors.None) {
38 if (verboseSsl) {
39 Log.Out ("[OpenID] Steam certificate: No error (1)");
40 }
41
42 return true;
43 }
44
45 X509Chain privateChain = new X509Chain ();
46 privateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
47
48 privateChain.ChainPolicy.ExtraStore.Add (caCert);
49 privateChain.ChainPolicy.ExtraStore.Add (caIntermediateCert);
50
51 if (privateChain.Build (new X509Certificate2 (_certificate))) {
52 // No errors, immediately return
53 privateChain.Reset ();
54 if (verboseSsl) {
55 Log.Out ("Steam certificate: No error (2)");
56 }
57
58 return true;
59 }
60
61 if (privateChain.ChainStatus.Length == 0) {
62 // No errors, immediately return
63 privateChain.Reset ();
64 if (verboseSsl) {
65 Log.Out ("Steam certificate: No error (3)");
66 }
67
68 return true;
69 }
70
71 // Iterate all chain elements
72 foreach (X509ChainElement chainEl in privateChain.ChainElements) {
73 if (verboseSsl) {
74 Log.Out ($"Validating cert: {chainEl.Certificate.Subject}");
75 }
76
77 // Iterate all status flags of the current cert
78 foreach (X509ChainStatus chainStatus in chainEl.ChainElementStatus) {
79 if (verboseSsl) {
80 Log.Out ($" Status: {chainStatus.Status}");
81 }
82
83 if (chainStatus.Status == X509ChainStatusFlags.NoError) {
84 // This status is not an error, skip
85 continue;
86 }
87
88 if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && chainEl.Certificate.Equals (caCert)) {
89 // This status is about the cert being an untrusted root certificate but the certificate is one of those we added, ignore
90 continue;
91 }
92
93 // This status is an error, print information
94 Log.Warning ($"[OpenID] Steam certificate error: {chainEl.Certificate.Subject} ### Error: {chainStatus.Status}");
95 privateChain.Reset ();
96 return false;
97 }
98 }
99
100 foreach (X509ChainStatus chainStatus in privateChain.ChainStatus) {
101 if (chainStatus.Status != X509ChainStatusFlags.NoError &&
102 chainStatus.Status != X509ChainStatusFlags.UntrustedRoot) {
103 Log.Warning ($"[OpenID] Steam certificate error: {chainStatus.Status}");
104 privateChain.Reset ();
105 return false;
106 }
107 }
108
109 // We didn't find any errors, chain is valid
110 privateChain.Reset ();
111 if (verboseSsl) {
112 Log.Out ("[OpenID] Steam certificate: No error (4)");
113 }
114
115 return true;
116 };
117 }
118
119 public static string GetOpenIdLoginUrl (string _returnHost, string _returnUrl) {
120 Dictionary<string, string> queryParams = new Dictionary<string, string> {
121 { "openid.ns", "http://specs.openid.net/auth/2.0" },
122 { "openid.mode", "checkid_setup" },
123 { "openid.return_to", _returnUrl },
124 { "openid.realm", _returnHost },
125 { "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select" },
126 { "openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select" }
127 };
128
129 return $"{STEAM_LOGIN}?{buildUrlParams (queryParams)}";
130 }
131
132 public static ulong Validate (HttpListenerRequest _req) {
133 string mode = getValue (_req, "openid.mode");
134 if (mode == "cancel") {
135 Log.Warning ("[OpenID] Steam OpenID login canceled");
136 return 0;
137 }
138
139 if (mode == "error") {
140 Log.Warning ($"[OpenID] Steam OpenID login error: {getValue (_req, "openid.error")}");
141 if (debugOpenId) {
142 PrintOpenIdResponse (_req);
143 }
144
145 return 0;
146 }
147
148 string steamIdString = getValue (_req, "openid.claimed_id");
149 ulong steamId;
150 Match steamIdMatch = steamIdUrlMatcher.Match (steamIdString);
151 if (steamIdMatch.Success) {
152 steamId = ulong.Parse (steamIdMatch.Groups [1].Value);
153 } else {
154 Log.Warning ("[OpenID] Steam OpenID login result did not give a valid SteamID");
155 if (debugOpenId) {
156 PrintOpenIdResponse (_req);
157 }
158
159 return 0;
160 }
161
162 Dictionary<string, string> queryParams = new Dictionary<string, string> {
163 { "openid.ns", "http://specs.openid.net/auth/2.0" },
164 { "openid.assoc_handle", getValue (_req, "openid.assoc_handle") },
165 { "openid.signed", getValue (_req, "openid.signed") },
166 { "openid.sig", getValue (_req, "openid.sig") },
167 { "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select" },
168 { "openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select" }
169 };
170
171 string[] signeds = getValue (_req, "openid.signed").Split (',');
172 foreach (string s in signeds) {
173 string name = $"openid.{s}";
174 queryParams [name] = getValue (_req, name);
175 }
176
177 queryParams.Add ("openid.mode", "check_authentication");
178
179 byte[] postData = Encoding.ASCII.GetBytes (buildUrlParams (queryParams));
180 HttpWebRequest request = (HttpWebRequest) WebRequest.Create (STEAM_LOGIN);
181 request.Method = "POST";
182 request.ContentType = "application/x-www-form-urlencoded";
183 request.ContentLength = postData.Length;
184 request.Headers.Add (HttpRequestHeader.AcceptLanguage, "en");
185 using (Stream st = request.GetRequestStream ()) {
186 st.Write (postData, 0, postData.Length);
187 }
188
189 HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
190 string responseString;
191 using (Stream st = response.GetResponseStream ()) {
192 using (StreamReader str = new StreamReader (st)) {
193 responseString = str.ReadToEnd ();
194 }
195 }
196
197 if (responseString.ContainsCaseInsensitive ("is_valid:true")) {
198 return steamId;
199 }
200
201 Log.Warning ($"[OpenID] Steam OpenID login failed: {responseString}");
202 return 0;
203 }
204
205 private static string buildUrlParams (Dictionary<string, string> _queryParams) {
206 string[] paramsArr = new string[_queryParams.Count];
207 int i = 0;
208 foreach ((string argName, string argValue) in _queryParams) {
209 paramsArr [i++] = $"{argName}={Uri.EscapeDataString (argValue)}";
210 }
211
212 return string.Join ("&", paramsArr);
213 }
214
215 private static string getValue (HttpListenerRequest _req, string _name) {
216 NameValueCollection nvc = _req.QueryString;
217 if (nvc [_name] == null) {
218 throw new MissingMemberException ($"[OpenID] OpenID parameter \"{_name}\" missing");
219 }
220
221 return nvc [_name];
222 }
223
224 private static void PrintOpenIdResponse (HttpListenerRequest _req) {
225 NameValueCollection nvc = _req.QueryString;
226 for (int i = 0; i < nvc.Count; i++) {
227 Log.Out ($" {nvc.GetKey (i)} = {nvc [i]}");
228 }
229 }
230 }
231}
Note: See TracBrowser for help on using the repository browser.