In working through the development of an Umbraco integration recently, I had a need to setup an OAuth authentication process to secure access to an API. At first glance, not too tricky – the provider had a well documented process to follow. However I ran into a few challenges when considering how this process can work for an Umbraco package – i.e. an integration that’s not for a single website, but for many, and for ones we don’t know the domains for at the time of configuration.
All in all it seemed worth a blog post.
The integration
Specifically, the integration I was working on was one between Umbraco Forms and HubSpot, the CRM platform. We wanted to have a custom workflow that editors could add to a form, map the fields in the form to the fields defined on a “contact” record at HubSpot, and then, when a form is submitted, use the available APIs to map save the form submission data as a HubSpot contact.
My colleague Warren worked on the original integration, which we released, open-sourced and blogged about how we built it.
HubSpot offers two authentication methods – using an API key and a OAuth process. We adopted the former, which was easier to integrate and likely suitable for a machine-to-machine, server-side integration such as that we had built.
This option is less recommended by HubSpot though, and in order to have your integration listed at their marketplace, it’s mandated to use the OAuth route.
Hence for an update to the package, we looked to add an option for the customer to configure and use the OAuth approach, should they prefer that option.
The OAuth prcess
The steps required for setting up OAuth for Hubspot are well documented, and are likely familiar if you’ve worked on similar integrations previously, as other providers will also follow this standard.
In summary, we need to handle the following steps:
- Have the user link to a specific URL provided by the platform, from where they are asked to login to their HubSpot account and agree to the requirement that the integration requests.
- These requirements are configured as scopes, and for our integration we required the ability to read and write contact information.
- The user is then redirected to a configured URL, where an authorization code is provided.
- This code can then be used in an API request to retrieve a pair of tokens – an access token and a refresh token.
- The access token has an expiry, but can be used for a while for subsequent API requests, authorizing the access to the API calls and resources agreed to by the user.
- The refresh token is longer lived, and can be used to request new access tokens, without going through the whole process again.
Adding OAuth to the package
The first step to add OAuth integration is to create and configure an app on HubSpot. Here we can provide details such as a name, logo and description that a user browsing the marketplace of available integration will see.
We can retrieve a client id and client secret, of which one or both need to be provided in the API calls we make for authentication and subsequent operation of the package.
Finally we configure the URL that the user will be returned to once they have signed into their account and agreed to the requested scopes.
And it was here we hit the first snag – there can only be one redirect URL configured, and it needs to be known, not at runtime when the package is installed, but now, at the time of configuring the app on HubSpot. This wouldn’t be a problem if we were configuring an integration for a single website, but of course for a package or product, installable by clients, it’s for many websites, not one, all of which are for domains we don’t know about.
Introducing a “stepping stone” website
Our solution to this was to decide to build and host a website, that can be used to receive these post-authentication requests, and configure that as the redirect URL.
With that we could set up the following process:
- On first using the package, the user can be determined as being unauthenticated by checking for a configured API key or a stored refresh token. If neither exists, they aren’t authenticated.
- We then prompt them to click off to the configured authentication URL.
- Once they’ve logged in and agreed to the requested scopes, they’ll be sent to our intermediate website.
- The authentication code is provided in a querystring, from where we can retrieve it and expose it clearly to the user so they can copy it to the clipboard.
So now we have an authorization code, what can we do with it? My first though, quickly abandoned, was that we could just ask the website implementor to save this code to their configuration file – in a similar way as to how we handled the initial API key based integration. This isn’t an option though, as, unlike the API key, the authorization code is very short lived. We do need to quickly go through the process of using it to generate an access and refresh token.
Instead of that, we would ask the user to paste the code into a field exposed on the Umbraco backoffice, click a button and from their we can make the API request to HubSpot to get the access token and refresh token.
The refresh token we save into the Umbraco database using the IKeyValueService. And the access token we save into an in-memory cache, such that we can use it in further API requests until it expires. If and when that happens, we can retrieve the refresh token from the database, get a new access token, retry the request and again cache the access token for future use.
Protecting the client secret
With that working, there was a further thing bothering me, which was that in the API requests made to retrieve tokens, we needed to provide the client secret in the request. And keeping this a secret seemed to be a problem.
As we’ve open sourced the integration, if we were to commit this key as a constant hard-coded into our application, it would be very easy to find it. Even if closed-source, via de-compiling it could still be discovered if someone was minded to do so.
Again we hit this problem as we are looking to build an integration package installable into multiple clients. If we had a single website to integrate with, we could just have this key safely in a configuration file or some other secrets store.
One option here would be to ask customers to set up their own HubSpot application. So rather than there being one that we configured, each project using the package would need to create their own. As well as being extra effort that we’d have to ask users of the package to make, it also meant we’d lose the ability to have an official Umbraco Forms app listed on the HubSpot marketplace.
The solution we found to this was to make use of the “stepping stone” website we already had in place. We exposed an API endpoint that our package token API requests would be configured to call, rather than going to the HubSpot URL. This API would be a relay to the HubSpot API, taking the request from the package, passing it on, and then similarly transferring the response back. You can see the source for this relay endpoint here.
The only thing it would add to the request would be the client secret, that the package wouldn’t know about, but the relaying web application could. We have the client secret now stored in a protected variable in our Azure DevOps pipeline, which is written as an application setting to the deployed Azure Web App. And as such it’s known to the web application, but not the installed packages and client’s Umbraco websites.
Smoothing out the flow
Now we’ve got a process that works, and is secure, but it suffers from being a slightly clunky flow for the user. We’ve got to ask them to copy and paste a code from one browser tab to another to complete the authentication. Which perhaps isn’t a huge deal, but still, it would be nicer if this process was more seamless.
And it turns out we could do that, making use of the browser’s ability to communicate between windows. By changing our initial link that kicks off the process from a standard anchor tag to a client-side request that uses window.open we could get a reference to the opener from the website that receives the authorization code.
Via that we can send a message with the authorization code to the opening window like this:
if (window.opener && typeof opener.postMessage === 'function') {
opener.postMessage({
type: 'hubspot:oauth:success',
url: location.href,
code: '@Model.AuthorizationCode'
}, '*');
window.close();
}
And in the opening window, running the integration in the Umbraco backoffice, we can receive this message, and use the code in the same way as if the user had copied and pasted it, and clicked the button, in the original flow:
window.addEventListener("message", function (event) {
if (event.data.type === "hubspot:oauth:success") {
// Use event.data.code.
}
}, false);
Summing up
By following the process described in this post we’ve got to a point where we now have an integration that can be used either with API key or OAuth based authentication as the user chooses. There are some challenges when setting up the latter for package that will be installed in many different locations, but by using an intermediate website and proxying API requests we can still adhere to this protocol and provide a smooth integration process for the user.
Comments
Post a Comment