MVC integration with FreshBooks Cloud Accounting

FreshBooks is my cloud accounting service that handles time tracking, expenses and invoicing. I was looking for a way to integrate the solution into my standard management app which has an MVC backend. Here's a quick breakdown of what you need to do to make this work.

FreshBooks APIs and app registrations

First, head over to the FreshBooks API help page to read the documentation and also go and register your app, and make sure to

  • Create a redirect URL to a public DNS site (localhost etc will not work)
  • Use a redirect handle from your planned controller action (I use /freshbooks to make this work)
  • Record all information after the app is created

I will put this info in my web.config as follows:

<!-- Settings for FreshbBooks integration -->
<add key="freshBooks:ClientId" value="ffbc4dc28f400ce78acc9652fd33a7893c6ed4a6a13b3c1226759bf7e589cd85" />
<add key="freshBooks:ClientSecret" value="3cb1fc7dd5e66abe8cd58f2c58e9406399e86229c21e85b5da90becf4cb1c203" />
<add key="freshBooks:RedirectUri" value="https://www.lekman.com/FreshBooks/callback" />
<add key="freshBooks:TimeOffset" value="1" />

The TimeOffset parameter is used later on, 1 means one hour difference from UTC in my case. Let's get back to that later.

First off, we need to be able to log on and generate OAuth tokens and persist these to a database. So first, I set up a code-first Entry Framework which I am not including here.

To get the token, the end user must click a link on the page. I generate that link as

/// <summary>
/// Gets the authorization URL.
/// </summary>
/// <value>The authorization URL.</value>
public static string AuthorizationUrl =>
    string.Format(
        "https://my.freshbooks.com/service/auth/oauth/authorize?client_id={0}&amp;response_type=code&amp;redirect_uri={1}",
        WebConfigurationManager.AppSettings["freshBooks:ClientId"],
        WebConfigurationManager.AppSettings["freshBooks:RedirectUri"]);

The end user will get a little button to click to launch the dialog for authentication. Next, the callback controller stores the token:

/// <summary>
/// Callback for FreshBooks to send a token to. Stores the value in database for the user.
/// </summary>
/// <returns>A redirect to the entry point controller.</returns>
public ActionResult Callback()
{
    if (Request.IsAuthenticated)
    {
        new FreshBooksModel().StoreAccessToken(Request.QueryString["code"]);
        return Redirect("/entry");
    }

    return new HttpUnauthorizedResult();
}

The heart of the integration lies in my HttpGet and HttpPost methods. I either GET or POST and serialize the results into typed objects. You can use the PostMan collection from FreshBooks to see these or you can call the method and specity "T" type as string to get plain JSON back. I then use json2csharp.com and generate classes and put these in another file. Here is the generated classes for the login token.

public class Token
{
    public string access_token { get; set; }
    public string token_type { get; set; }
    public int expires_in { get; set; }
    public string refresh_token { get; set; }
    public int created_at { get; set; }
}

I use this object in my generic HttpPost method which looks like this:

/// <summary>
/// Ensures that the security protocols for SSL and TLS are activated.
/// </summary>
private static void EnsureSecurity()
{
    // Append TLS SSL settings, some servers cannot create secure channel
    ServicePointManager.Expect100Continue = true;
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11
                                                                    | SecurityProtocolType.Tls12
                                                                    | SecurityProtocolType.Ssl3;
}

/// <summary>
/// Performs a HTTP POST message to the target URL with form content and optional headers.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="url">The URL.</param>
/// <param name="form">The form.</param>
/// <param name="headers">The headers.</param>
/// <param name="token">The token.</param>
/// <returns>
/// The response body as a typed object or raw JSON if type is string.
/// </returns>
private T HttpPost<T>(string url, KeyValuePair<string, string>[] form,
    KeyValuePair<string, string>[] headers = null, string token = null)
{
    EnsureSecurity();

    using (var client = new HttpClient())
    {
        using (var content = new FormUrlEncodedContent(form))
        {
            if (headers != null)
            {
                foreach (var header in headers)
                {
                    content.Headers.Add(header.Key, header.Value);
                }
            }

            if (token != null)
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                client.DefaultRequestHeaders.ExpectContinue = false;
            }

            using (var result = client.PostAsync(url, content).Result)
            {
                if (result.StatusCode == HttpStatusCode.Unauthorized)
                {
                    if (string.IsNullOrEmpty(AccessToken) || _tokenRefreshed)
                    {
                        Authenticated = false;
                        return default;
                    }

                    RefreshAccessToken();
                    return HttpPost<T>(url, form, headers, token);
                }

                var text = result.Content.ReadAsStringAsync().Result;
                return typeof(T) == typeof(string)
                    ? (T) Convert.ChangeType(text, typeof(T))
                    : (T) JsonConvert.DeserializeObject(text, typeof(T));
            }
        }
    }
}

The function starts with calling EnsureSecurity to register TLS settings. If you run the base code on Azure then you will get SSL failues due to incorrect HTTPOS handhakes. We then pass a URL, a set of key/value parameters that will contain the form post, optional headers to send and an OAuth token. As this is the time when we generate the token, then we have nothing to send. 

The call to generate the token looks like this:

/// <summary>
/// Stores the access token.
/// </summary>
/// <param name="code">The code.</param>
public void StoreAccessToken(string code)
{
    using (var db = new DataModel())
    {
        using (var cache = new Cache("FreshBooksCache", CurrentProfile, db))
        {
            cache.Set(HttpPost<Token>(
                "https://api.freshbooks.com/auth/oauth/token", new[]
                {
                    new KeyValuePair<string, string>("grant_type", "authorization_code"),
                    new KeyValuePair<string, string>("client_id",
                        WebConfigurationManager.AppSettings["freshBooks:ClientId"]),
                    new KeyValuePair<string, string>("client_secret",
                        WebConfigurationManager.AppSettings["freshBooks:ClientSecret"]),
                    new KeyValuePair<string, string>("code", code),
                    new KeyValuePair<string, string>("redirect_uri",
                        WebConfigurationManager.AppSettings["freshBooks:RedirectUri"])
                }
                , new[]
                {
                    new KeyValuePair<string, string>("Api-Version", "alpha")
                }));
        }
    }
}

The token only lives for 12 hours so we need to create a refresh token when the user logs in on the day after authorizxing. This happens in the RefreshAccessToken method as

/// <summary>
/// Refreshes the access token.
/// </summary>
private void RefreshAccessToken()
{
    using (var db = new DataModel())
    {
        using (var cache = new Cache("FreshBooksCache", CurrentProfile, db))
        {
            var token = cache.Get<Token>();

            token = HttpPost<Token>(
                "https://api.freshbooks.com/auth/oauth/token", new[]
                {
                    new KeyValuePair<string, string>("grant_type", "refresh_token"),
                    new KeyValuePair<string, string>("client_id",
                        WebConfigurationManager.AppSettings["freshBooks:ClientId"]),
                    new KeyValuePair<string, string>("client_secret",
                        WebConfigurationManager.AppSettings["freshBooks:ClientSecret"]),
                    new KeyValuePair<string, string>("refresh_token", token.refresh_token),
                    new KeyValuePair<string, string>("redirect_uri",
                        WebConfigurationManager.AppSettings["freshBooks:RedirectUri"])
                }
                , new[]
                {
                    new KeyValuePair<string, string>("Api-Version", "alpha")
                });
            _tokenRefreshed = true;
            AccessToken = token.access_token;
            cache.Set(token);
        }
    }
}

If all else fails, then we set the Authenticated property to false and show the login link and an error message.

The call to "cache.Set" is a wrapper for the entity framework database calls where we get serialized objects based on the login information of the user. Similarly, I get the token later on using cache.Get both in refresh and later on for getting data.

I have a lot of different methods, but here are two examples. I have a quick component in the mobile app that allows you to register timesheets and expenses as:

We need to get project as follows, and here is where I use the time offset to move the date by hours and get them in correct day:

/// <summary>
/// Performs a HTTP GET request  to the target URL.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="url">The URL.</param>
/// <returns>The response body as a typed object or raw JSON if type is string.</returns>
private T HttpGet<T>(string url)
{
    EnsureSecurity();

    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
        client.DefaultRequestHeaders.ExpectContinue = false;
        using (var result = client.GetAsync(url).Result)
        {
            if (result.StatusCode == HttpStatusCode.Unauthorized)
            {
                if (string.IsNullOrEmpty(AccessToken) || _tokenRefreshed)
                {
                    Authenticated = false;
                    return default;
                }

                RefreshAccessToken();
                return HttpGet<T>(url);
            }

            var text = result.Content.ReadAsStringAsync().Result;
            return typeof(T) == typeof(string)
                ? (T)Convert.ChangeType(text, typeof(T))
                : (T)JsonConvert.DeserializeObject(text, typeof(T));
        }
    }
}

/// <summary>
/// Gets the projects.
/// </summary>
/// <returns>A <see cref="ProjectModel"/> reference.</returns>
private ProjectModel GetProjects()
{
    return HttpGet<ProjectModel>(
        $"https://api.freshbooks.com/projects/business/{Identity.business_memberships[0].business.id}/projects");
}

We can then save projects using:

/// <summary>
/// Gets the identity.
/// </summary>
/// <returns>An <see cref="Identity"/> reference.</returns>
private Identity GetIdentity() =>
    HttpGet<IdentityModel>("https://api.freshbooks.com/auth/api/v1/users/me").response;

/// <summary>
/// Adds the time entry.
/// </summary>
/// <param name="creation">The creation.</param>
/// <param name="identity">The identity.</param>
/// <param name="token">The token.</param>
/// <returns>The unique identifier of the new entry.</returns>
public int AddTimeEntry(TimeEntryCreation creation, Identity identity, string token)
{
    if (identity == null)
    {
        identity = GetIdentity();
    }

    const string url =
        "https://api.freshbooks.com/timetracking/business/{0}/time_entries";
    var business = identity.business_memberships[0].business.id;

    EnsureSecurity();
    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        client.DefaultRequestHeaders.ExpectContinue = false;

        var payload = JsonConvert.SerializeObject(creation);
        var content = new StringContent(payload, Encoding.UTF8, "application/json");

        using (var result = client.PostAsync(string.Format(url, business), content).Result)
        {
            var text = result.Content.ReadAsStringAsync().Result;
            return ((TimeEntryCreationResult) JsonConvert.DeserializeObject(text,
                typeof(TimeEntryCreationResult))).time_entry.id;
        }
    }
}

Hope this is a good start for anyone!