Skip to content

Instantly share code, notes, and snippets.

@drysart
Created October 11, 2023 22:54
Show Gist options
  • Save drysart/5c11ae7cf8af5479f27775af5f438315 to your computer and use it in GitHub Desktop.
Save drysart/5c11ae7cf8af5479f27775af5f438315 to your computer and use it in GitHub Desktop.
Working C# MyQ login code
using System.Net;
using System.Security.Cryptography;
using System.Web;
namespace MyQLogin
{
public static class Program
{
const string MYQ_API_CLIENT_ID = "ANDROID_CGI_MYQ";
const string MYQ_API_CLIENT_SECRET = "VUQ0RFhuS3lQV3EyNUJTdw==";
const string MYQ_API_REDIRECT_URI = "com.myqops://android";
// Should have fallback login to try -east and -west domains if the main domain fails on either of the /connect/* endpoints
const string BASE_URL = "https://partner-identity.myq-cloud.com";
const string AUTHORIZATION_URL = BASE_URL + "/connect/authorize";
const string GET_TOKEN_URL = BASE_URL + "/connect/token";
const string username = "YOUR USERNAME HERE";
const string password = "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;
//}
}
}
}
// WTF?
loginForm["Brand"] = "myq";
loginForm["UnifiedFlowRequested"] = "True";
// # 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