-
-
Save drysart/22b1761998b5a744f480f4590aba7294 to your computer and use it in GitHub Desktop.
MyQ login code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Net; | |
using System.Security.Cryptography; | |
using System.Web; | |
namespace MyQLogin | |
{ | |
public static class Program | |
{ | |
const string MYQ_API_CLIENT_ID = "IOS_CGI_MYQ"; | |
const string MYQ_API_CLIENT_SECRET = "VUQ0RFhuS3lQV3EyNUJTdw=="; | |
const string MYQ_API_REDIRECT_URI = "com.myqops://ios"; | |
// Currently this works on -east; but not the unregioned domain. | |
const string BASE_URL = "https://partner-identity-east.myq-cloud.com"; | |
const string AUTHORIZATION_URL = BASE_URL + "/connect/authorize"; | |
const string GET_TOKEN_URL = BASE_URL + "/connect/token"; | |
const string username = "PUT YOUR USERNAME HERE"; | |
const string password = "PUT YOUR PASSWORD HERE"; | |
private struct PkceChallenge | |
{ | |
public string CodeVerifier { get; set; } | |
public string CodeChallenge { get; set; } | |
} | |
private static string GeneratePkceVerifier(int length) | |
{ | |
byte[] buf = new byte[length]; | |
var r = new Random(); | |
r.NextBytes(buf); | |
return Convert.ToBase64String(buf) | |
.Replace("/", "_") | |
.Replace("+", "-") | |
.Replace("=", ""); | |
} | |
private static string GeneratePkceCodeChallenge(string verifier) | |
{ | |
var hash = SHA256.Create().ComputeHash(System.Text.Encoding.ASCII.GetBytes(verifier)); | |
var result = Convert.ToBase64String(hash) | |
.Replace("/", "_") | |
.Replace("+", "-") | |
.Replace("=", ""); | |
return result; | |
} | |
private static PkceChallenge GeneratePkceChallenge(int? length) | |
{ | |
length = length ?? 32; | |
var verifier = GeneratePkceVerifier(length.Value); | |
var challenge = GeneratePkceCodeChallenge(verifier); | |
return new PkceChallenge { CodeVerifier = verifier, CodeChallenge = challenge }; | |
} | |
private static HttpClient CreateRedirectingHttpClient() | |
{ | |
var directHandler = new HttpClientHandler() { AllowAutoRedirect = true }; | |
var directClient = new HttpClient(directHandler); | |
return directClient; | |
} | |
private static HttpClient CreateDirectHttpClient() | |
{ | |
var directHandler = new HttpClientHandler() { AllowAutoRedirect = false}; | |
var directClient = new HttpClient(directHandler); | |
return directClient; | |
} | |
public static async Task<int> Main(string[] args) | |
{ | |
var pkce = GeneratePkceChallenge(null); | |
IEnumerable<string> cookies; | |
// # retrieve authentication page | |
string loginPageUrl; | |
string loginPageHtml; | |
{ | |
using var redirectingClient = CreateRedirectingHttpClient(); | |
var queryParams = new Dictionary<string, string> | |
{ | |
{ "client_id", MYQ_API_CLIENT_ID }, | |
{ "code_challenge", pkce.CodeChallenge }, | |
{ "code_challenge_method", "S256" }, | |
{ "redirect_uri", MYQ_API_REDIRECT_URI }, | |
{ "response_type", "code" }, | |
{ "scope", "MyQ_Residential offline_access" } | |
}; | |
var qstr = String.Join( | |
"&", | |
queryParams.Select(kvp => $"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}") | |
); | |
var req = new HttpRequestMessage(HttpMethod.Get, AUTHORIZATION_URL + "?" + qstr); | |
var resp = await redirectingClient.SendAsync(req); | |
if (resp.StatusCode == (HttpStatusCode)429) | |
{ | |
throw new Exception("rate limited"); | |
} | |
resp.EnsureSuccessStatusCode(); | |
loginPageUrl = resp.RequestMessage.RequestUri.ToString(); | |
loginPageHtml = await resp.Content.ReadAsStringAsync(); | |
cookies = resp.Headers.GetValues("Set-Cookie"); | |
} | |
// # Scanning returned web page for required fields. | |
string loginUrlString; | |
var loginForm = new Dictionary<string, string>(); | |
{ | |
var loginPageDoc = new HtmlAgilityPack.HtmlDocument(); | |
loginPageDoc.LoadHtml(loginPageHtml); | |
var formEl = loginPageDoc.DocumentNode.SelectSingleNode("//form"); | |
var formAction = HttpUtility.HtmlDecode(formEl.GetAttributeValue("action", "")); | |
var formMethod = HttpUtility.HtmlDecode(formEl.GetAttributeValue("method", "")); | |
loginUrlString = formAction; | |
foreach (var inputEl in loginPageDoc.DocumentNode.SelectNodes("//input")) | |
{ | |
var name = HttpUtility.HtmlDecode(inputEl.GetAttributeValue("name", "")); | |
var value = HttpUtility.HtmlDecode(inputEl.GetAttributeValue("value", "")); | |
Console.WriteLine($"{name}\t{value}"); | |
switch (name.ToLower()) | |
{ | |
case "email": | |
value = Program.username; | |
break; | |
case "password": | |
value = Program.password; | |
break; | |
default: | |
break; | |
} | |
if (!String.IsNullOrWhiteSpace(value) && !String.IsNullOrWhiteSpace(name)) | |
{ | |
if (name.ToLower() != "returnurl") | |
{ | |
loginForm[name] = value; | |
} | |
} | |
} | |
} | |
// # Perform login to MyQ | |
{ | |
using var directClient = CreateDirectHttpClient(); | |
var u = new Uri(new Uri(loginPageUrl), /*new Uri(BASE_URL),*/ loginUrlString); | |
var req = new HttpRequestMessage(HttpMethod.Post, u.ToString()); | |
req.Content = new FormUrlEncodedContent(loginForm); | |
TrimSetCookies(req, cookies); | |
var resp = await directClient.SendAsync(req); | |
if (resp.StatusCode == (HttpStatusCode)200) | |
{ | |
var body = await resp.Content.ReadAsStringAsync(); | |
throw new Exception(body); | |
} | |
if (resp.StatusCode != (HttpStatusCode)302) { throw new Exception(""); } | |
cookies = resp.Headers.GetValues("Set-Cookie"); | |
} | |
// ??? | |
// # Intercept redirect back to MyQ iOS app | |
string code; | |
string scope; | |
{ | |
using var redirectingClient = CreateRedirectingHttpClient(); | |
var queryParams = new Dictionary<string, string> | |
{ | |
{ "client_id", MYQ_API_CLIENT_ID }, | |
{ "code_challenge", pkce.CodeChallenge }, | |
{ "code_challenge_method", "S256" }, | |
{ "redirect_uri", MYQ_API_REDIRECT_URI }, | |
{ "response_type", "code" }, | |
{ "scope", "MyQ_Residential offline_access" } | |
}; | |
var qstr = String.Join( | |
"&", | |
queryParams.Select(kvp => $"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}") | |
); | |
var req = new HttpRequestMessage(HttpMethod.Get, AUTHORIZATION_URL + "?" + qstr); | |
TrimSetCookies(req, cookies); | |
var resp = await redirectingClient.SendAsync(req); | |
var redirectTargetLocation = resp.Headers.Location!; | |
var queryParamsr = HttpUtility.ParseQueryString(redirectTargetLocation.Query); | |
code = queryParamsr["code"]!; | |
scope = queryParamsr["scope"]!; | |
} | |
// # Retrieve token | |
{ | |
using var directClient = CreateDirectHttpClient(); | |
var req = new HttpRequestMessage(HttpMethod.Post, GET_TOKEN_URL); | |
var decodedClientSecret = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(MYQ_API_CLIENT_SECRET)); | |
req.Content = new FormUrlEncodedContent(new Dictionary<string, string> | |
{ | |
{ "client_id", MYQ_API_CLIENT_ID }, | |
{ "client_secret", decodedClientSecret }, | |
{ "code", code }, | |
{ "code_verifier", pkce.CodeVerifier }, | |
{ "grant_type", "authorization_code" }, | |
{ "redirect_uri", MYQ_API_REDIRECT_URI }, | |
{ "scope", scope } | |
}); | |
req.Headers.Add("Accept", "application/json"); | |
req.Headers.Add("User-Agent", "Apple/iOS 5.242.0.38913"); | |
var resp = await directClient.SendAsync(req); | |
resp.EnsureSuccessStatusCode(); | |
var respJson = await resp.Content.ReadAsStringAsync(); | |
} | |
return 0; | |
} | |
private static void TrimSetCookies(HttpRequestMessage req, IEnumerable<string> cookies) | |
{ | |
var cstr = String.Join("; ", cookies.Select(x => x.Split(";")[0])); | |
req.Headers.Add("Cookie", cstr); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment