Reputation: 860
I tried automating the login for Exact Online using the c# HttpClient. I am stuck at the form to fill in the one time password. Generating the OTP is easy, I took the DEVICE_SECRET_KEY during the first time login and stored that secret.
The code to generate this password is correct. When I use the website using username + password + generate a key. I can login. I would really like to automate this process. Does anyone see the flaw I made?
I am currently using selenium. That works, but I that would require a firefox installation on a production server. Which I would rather not do.
This is my code. Step 1 => step 2 works, the username + password are sent correctly. The second step, entering the key and getting to the redirecturl does not seem to work.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using HtmlAgilityPack;
using HtmlAgilityPack.CssSelectors.NetCore;
using OtpNet;
namespace Om.Finance.TotpGenerator
{
class Program
{
static void Main(string[] args)
{
using (var client = new HttpClient())
{
//step 1 initial call to get CSRFTOKEN
var url1 = "https://start.exactonline.nl/api/oauth2/auth/?client_id=_______&redirect_uri=_____&response_type=code";
var logInScreen = client.GetStringAsync(url1 + "&force_login=1").Result;
var loginDoc = new HtmlDocument();
loginDoc.LoadHtml(logInScreen);
var csrfToken = loginDoc.DocumentNode.QuerySelector("input[name='CSRFToken']").GetAttributeValue("value", "");
var viewState = loginDoc.DocumentNode.QuerySelector("input[name='__VIEWSTATE']").GetAttributeValue("value", "");
var viewStateGenerator = loginDoc.DocumentNode.QuerySelector("input[name='__VIEWSTATEGENERATOR']").GetAttributeValue("value", "");
var eventValidation = loginDoc.DocumentNode.QuerySelector("input[name='__EVENTVALIDATION']").GetAttributeValue("value", "");
//first a get to retrieve the CSRFToken and all other post variables
//then post the data to the url
var postVariables = new Dictionary<string, string>
{
{ "CSRFToken", csrfToken },
{"sysFocus", ""},
{"_Division_", "1"},
{"__VIEWSTATE", viewState},
{"SysNoBack", "1"},
{"__VIEWSTATEGENERATOR", viewStateGenerator},
{"__EVENTTARGET", ""},
{"__EVENTARGUMENT", ""},
{"__EVENTVALIDATION", eventValidation},
{"Attempts", "0"},
{"Action", "0"},
{"UserNameField", "USERNAME"},
{"PasswordField", "PASSWORD"},
{"LoginButton", "Inloggen"},
{"hf2StepLoginStep", "0"},
};
//post to url1
var otpInput = client.PostAsync(url1, new FormUrlEncodedContent(postVariables)).Result;
//process this page as well
var keyInput = new HtmlDocument();
keyInput.LoadHtml(otpInput);
csrfToken = keyInput.DocumentNode.QuerySelector("input[name='CSRFToken']").GetAttributeValue("value", "");
viewState = keyInput.DocumentNode.QuerySelector("input[name='__VIEWSTATE']").GetAttributeValue("value", "");
viewStateGenerator = keyInput.DocumentNode.QuerySelector("input[name='__VIEWSTATEGENERATOR']").GetAttributeValue("value", "");
eventValidation = keyInput.DocumentNode.QuerySelector("input[name='__EVENTVALIDATION']").GetAttributeValue("value", "");
var division = keyInput.DocumentNode.QuerySelector("input[name='_Division_']").GetAttributeValue("value", "");
var key = Base32Encoding.ToBytes("FROM THE QRCODE TO SCAN ON FIRST LOGIN");
var totp = new Totp(key);
var rst = totp.ComputeTotp();
Console.WriteLine(rst);
var postVariables2 = new Dictionary<string, string>
{
{ "CSRFToken", csrfToken },
{"sysFocus", ""},
{"_Division_", division},
{"__EVENTTARGET", ""},
{"__EVENTARGUMENT", ""},
{"__VIEWSTATE", viewState},
{"SysNoBack", "1"},
{"__VIEWSTATEGENERATOR", viewStateGenerator},
{"__EVENTVALIDATION", eventValidation},
{"Attempts", "0"},
{"Action", "0"},
{"ResponseTokenTotp$Key", rst},
{"LoginButton", "Inloggen"},
{"hf2StepLoginStep", "0"},
};
var response2 = client.PostAsync(url1, new FormUrlEncodedContent(postVariables2)).Result;
//response2 should contain a REQUEST MESSAGE TO THE CALLBACK URL, INSTEAD IN BACK AT ENTERING the OTP
}
}
}
}
Upvotes: 1
Views: 655
Reputation: 1
I have run into this issue as well, and managed to find a solution to this issue.
When attempting to automate web based behavior it is sometimes important to carefully look at how the request is constructed and what parameters are required. I personally use Fiddler to see all the data that is sent, through which I can replicate the necessary actions.
The problem this question most likely encountered was forgetting to properly set the request headers.
Here I have provided a sample code that allows the user to log into exact online using the httpclient of C#:
private async Task<string> Login()
{
string client_id = "CLIENTID";
string client_secret = "CLIENTSECRET";
string redirect_url = "REDIRECTURI";
string url = $"https://start.exactonline.nl/api/oauth2/auth?client_id={client_id}&redirect_uri={redirect_url}&response_type=code&force_login=1";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.ConnectionClose = false; // Make sure connection is keepalive
// Since we're replicating web behavior, add default information
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xhtml+xml"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml", 0.9));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/avif"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/webp"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*", 0.8));
client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US"));
client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en", 0.5));
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; AcmeInc/1.0)");
HttpResponseMessage initial_response = await client.GetAsync(url);
HtmlDocument html_initial = new HtmlDocument();
html_initial.LoadHtml(await initial_response.Content.ReadAsStringAsync());
string token = html_initial.DocumentNode.SelectSingleNode("//input[@name='__RequestVerificationToken']")?.GetAttributeValue("value", null) ?? "";
string? code = null;
Console.ForegroundColor = ConsoleColor.Yellow;
while (String.IsNullOrEmpty(code))
{
try
{
Console.WriteLine("Enter Exact credentials.");
Console.WriteLine("Exact username (E-mail):");
string? email = Console.ReadLine();
HttpResponseMessage account_response = await client.PostAsync(
url,
new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("UserNameField", email ?? ""),
new KeyValuePair<string, string>("__RequestVerificationToken", token),
}));
HtmlDocument html_account = new HtmlDocument();
html_account.LoadHtml(await account_response.Content.ReadAsStringAsync());
Console.WriteLine("\nExact password:");
string? password = Console.ReadLine();
HttpResponseMessage login_response = await client.PostAsync(
account_response.RequestMessage?.RequestUri?.ToString() ?? "",
new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("CSRFToken" , html_account.DocumentNode.SelectSingleNode("//input[@name='CSRFToken']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("sysFocus" , html_account.DocumentNode.SelectSingleNode("//input[@name='sysFocus']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("_Division_" , html_account.DocumentNode.SelectSingleNode("//input[@name='_Division_']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("Stampname" , html_account.DocumentNode.SelectSingleNode("//input[@name='StampName']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__VIEWSTATE" , html_account.DocumentNode.SelectSingleNode("//input[@name='__VIEWSTATE']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("SysNoBack" , html_account.DocumentNode.SelectSingleNode("//input[@name='SysNoBack']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__VIEWSTATEGENERATOR" , html_account.DocumentNode.SelectSingleNode("//input[@name='__VIEWSTATEGENERATOR']")?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__EVENTTARGET" , html_account.DocumentNode.SelectSingleNode("//input[@name='__EVENTTARGET']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__EVENTARGUMENT" , html_account.DocumentNode.SelectSingleNode("//input[@name='__EVENTARGUMENT']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__EVENTVALIDATION" , html_account.DocumentNode.SelectSingleNode("//input[@name='__EVENTVALIDATION']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("Attempts" , html_account.DocumentNode.SelectSingleNode("//input[@name='Attempts']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("Action" , html_account.DocumentNode.SelectSingleNode("//input[@name='Action']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("UserNameField" , email ?? ""),
new KeyValuePair<string, string>("PasswordField" , password ?? ""),
new KeyValuePair<string, string>("LoginButton" , html_account.DocumentNode.SelectSingleNode("//input[@name='LoginButton']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("hf2StepLoginStep" , html_account.DocumentNode.SelectSingleNode("//input[@name='hf2StepLoginStep']") ?.GetAttributeValue("value", null) ?? ""),
}));
if (login_response.RequestMessage.RequestUri.ToString().Contains("?code=")) // Not all accounts have 2FA
code = login_response.RequestMessage.RequestUri.ToString().Substring(redirect_url.Length + 6);
else
{
// Get 2FA Code
HtmlDocument html_totp = new HtmlDocument();
html_totp.LoadHtml(await login_response.Content.ReadAsStringAsync());
if (html_totp.DocumentNode.SelectSingleNode("//input[@name='ResponseTokenTotp$Key']") == null) // Initial auth failed
throw new Exception("The username or password is incorrect.");
while (String.IsNullOrEmpty(code))
{
Console.WriteLine("Exact totp code:");
string? totp_code = Console.ReadLine();
HttpResponseMessage totp_response = await client.PostAsync(
login_response.RequestMessage?.RequestUri?.ToString() ?? "",
new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("CSRFToken" , html_totp.DocumentNode.SelectSingleNode("//input[@name='CSRFToken']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("sysFocus" , html_totp.DocumentNode.SelectSingleNode("//input[@name='sysFocus']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("_Division_" , html_totp.DocumentNode.SelectSingleNode("//input[@name='_Division_']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("StampName" , html_totp.DocumentNode.SelectSingleNode("//input[@name='StampName']") ?.GetAttributeValue("value", null) ?? "NL001"),
new KeyValuePair<string, string>("__EVENTTARGET" , html_totp.DocumentNode.SelectSingleNode("//input[@name='__EVENTTARGET']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__EVENTARGUMENT" , html_totp.DocumentNode.SelectSingleNode("//input[@name='__EVENTARGUMENT']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__VIEWSTATE" , html_totp.DocumentNode.SelectSingleNode("//input[@name='__VIEWSTATE']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("SysNoBack" , html_totp.DocumentNode.SelectSingleNode("//input[@name='SysNoBack']") ?.GetAttributeValue("value", null) ?? "1"),
new KeyValuePair<string, string>("__VIEWSTATEGENERATOR" , html_totp.DocumentNode.SelectSingleNode("//input[@name='__VIEWSTATEGENERATOR']")?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("__EVENTVALIDATION" , html_totp.DocumentNode.SelectSingleNode("//input[@name='__EVENTVALIDATION']") ?.GetAttributeValue("value", null) ?? ""),
new KeyValuePair<string, string>("Attempts" , html_totp.DocumentNode.SelectSingleNode("//input[@name='Attempts']") ?.GetAttributeValue("value", null) ?? "0"),
new KeyValuePair<string, string>("Action" , html_totp.DocumentNode.SelectSingleNode("//input[@name='Action']") ?.GetAttributeValue("value", null) ?? "0"),
new KeyValuePair<string, string>("ResponseTokenTotp$Key", totp_code ?? ""),
new KeyValuePair<string, string>("LoginButton" , html_totp.DocumentNode.SelectSingleNode("//input[@name='LoginButton']") ?.GetAttributeValue("value", null) ?? "Volgende"),
new KeyValuePair<string, string>("hf2StepLoginStep" , html_totp.DocumentNode.SelectSingleNode("//input[@name='hf2StepLoginStep']") ?.GetAttributeValue("value", null) ?? "0"),
}));
if (totp_response.RequestMessage.RequestUri.ToString().Contains("?code="))
code = totp_response.RequestMessage.RequestUri.ToString().Substring(redirect_url.Length + 6);
else
Console.WriteLine("Totp code was incorrect!");
}
}
}
catch(Exception ex) {
Console.WriteLine(ex.Message);
}
}
Console.ForegroundColor = ConsoleColor.White;
{
HttpResponseMessage response = await client.PostAsync(
"https://start.exactonline.nl/api/oauth2/token",
new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("client_id", client_id),
new KeyValuePair<string, string>("client_secret", client_secret),
new KeyValuePair<string, string>("code", Uri.UnescapeDataString(code)),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("redirect_uri", redirect_url)
}));
var responseContent = await response.Content.ReadAsStringAsync();
Dictionary<string, object> resp = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(responseContent);
// Add some logic to save the refresh token
return resp["access_token"].ToString();
}
}
}
The code isn't perfect but I hope this answer will help someone in the future.
Upvotes: 0