André
André

Reputation: 860

Exact Online Automating Login

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

Answers (1)

RensBe
RensBe

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

Related Questions