How Hashnode implements SSO for blogs running on custom domains

Sandeep Panda

·

·

5.2K views

As you may know, Hashnode is a community of independent developers who blog on their own domain. Hashnode users discover content published by the bloggers on their feed. Since our users are logged in on Hashnode and not the custom domains where the articles live, we had to carry the session forward somehow.

For example, I am logged in on Hashnode, but I am reading a content published on a different Hashnode powered domain. For example:

How do we ensure that my session reflects on this domain so that I can like the post or write a comment?

SSO on our own cloud

Initially we relied on third party cookies to do auth. We waited for the page to load fully and then made an API call to Hashnode to fetch the currently logged in user. It worked quite well. However, we soon realized that Safari blocks all third-party cookies! So, we decided to implement SSO on Hashnode and activated it only for Safari users. Here is the flow:

  • User requests a blog and it hits one of our edge servers.
  • We detect if the user is on Safari. In that case we redirect them to https://hashnode.com/identity and pass a next query param which is the blog URL.
  • Hashnode checks for a login cookie. If found, we create a short lived token for the user and store it against a guid. We redirect the user back to the blog by appending guid as a query param.
  • After being redirected, the blog renders and our JavaScript code pulls in the guid and exchanges it with our server for an auth token.
  • Subsequent API calls use the auth token and send it in x-auth-token header to authenticate with Hashnode endpoints.

hn-authentication-chart.png

This worked quite well, but as you may have guessed the redirection process took a bit of time. Our edge caching didn't really matter since we redirected the requests to Hashnode before serving the content. But this flow was in place just for Safari. For Chrome, Firefox and other browsers, we served content straightaway from our edge cache and used third-party cookies to communicate with Hashnode.

However, we soon realized that Brave browser also blocks third-party cookies when privacy shield is on. Also, we didn't have any reliable way of knowing whether the request came from Brave browser. The user-agent header simply didn't give any info. A few days later, Chrome also joined the bandwagon and started blocking third-party cookies. Now our only choice was to redirect every request intended for Hashnode-powered domains to https://hashnode.com first and obtain auth token -- that meant we could no longer take advantage of our CDN. SSO would make the blogs slower.

After a few sleepless nights, I thought why not use Cloudflare Workers for implementing SSO? Worker scripts run in 150+ data centres across the globe and can handle millions of redirections easily! The best part is that the requests will be sent to a DC that is closest to the users. This means we can implement an SSO feature without sacrificing the speed and benefits of our own CDN.

So, last week I sat down to build this. Here is how it works:

SSO on Cloudflare Workers

As I mentioned above, we already have a home grown CDN which routes user requests to locations that are geographically closest to our users. Since Workers also factor in geo proximity while serving requests, we came up with the following steps to implement SSO. It's largely same as our previous setup, but the redirection part is handled by Cloudflare Workers.

  • Whenever a request comes for any specific domain in our network (e.g. https://sandeep.dev), we redirect the user to https://hashnode.com/identity which is powered by Workers. It passes the current hostname in a query param called next.

  • The script checks if a login cookie is present in the request. If yes, it generates a short lived JWT token and a GUID.

  • The script stores the JWT against the generated GUID using Workers KV Store. It redirects the user to URL passed in next query param and sends the GUID along with it.

  • After redirection happens, the blog exchanges the guid for the short lived JWT and uses it to authenticate API calls.

Here is how the worker script looks like:

let next = getQueryParam(request, 'next'); // Extract value of next query param from the URL

if (!next) { // If nothing is supplied then redirect to Hashnode
    return Response.redirect(`https://hashnode.com`, 302);
}

next = decodeURIComponent(next);
// check if the login cookie is present. If yes, the user is logged in to Hashnode
const cookie = getCookie(request, 'login.jwt');

if (!cookie) { // If no login cookie is present, the user is unauthenticated
    return Response.redirect(`${next}?guid=none`, 302);
}

const decodedJwt = jwt.verify(cookie, jwtSecret); // Decode the JWT using secret
const userId = decodedJwt.data.userId; // Find the id of the user

const data = {
    userId: userId,
    expires: new Date().getTime() + (cookieTTL * 3600 * 1000),
    v: 1
}

const signedToken = jwt.sign(data, jwtSecret); // Create a new JWT that lives for 6 hours
const guid = uuidv4(); // Create a GUID
await guids.put(guid, signedToken, { expirationTtl: 60 }); // Store the JWT against a GUID which is valid for 60s

return Response.redirect(`${next}?guid=${guid}`, 302); // Finally, redirect the user back to where they came from

Once the client extracts the GUID, it makes an API call and retrieves the JWT. Here's workers script for that accepts the GUID and responds back with the JWT:

const token = await guids.get(guidFromQuery); // retrieves the value from Workers KV
const origin = request.headers.get('origin');

const init = {
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': origin,
    'Access-Control-Allow-Headers': 'Content-Type'
  }
}
guids.delete(guidFromQuery); // Delete the key once retrieved
return new Response(JSON.stringify({ token: token }), init);

It has been working out really well so far. We don't suffer if third party cookies are blocked in certain browsers. The performance doesn't degrade because the initial redirection is based on geo proximity. For example, if someone is accessing sandeep.dev from Bangalore then the initial redirection always goes to Bangalore/Singapore. Then the actual content is served from Bangalore.

If we take a look at the network tab, we'll notice that the TTFB value is still < 500ms most of the times. The redirection is short and consumes < 300ms. While this setup is slightly slower than serving content from CDN directly (without SSO), it solves one big problem which is Authentication. So, the slight reduction in speed is acceptable.

Screenshot 2020-07-05 at 7.10.50 PM.png

The best part is that Workers is handling this gracefully with 100% success rate so far.

Screenshot 2020-07-05 at 7.14.17 PM.png


This is the first time I used Cloudflare Workers in a prod set up. You should definitely check it out if you have a similar use case. Additionally, do checkout KV store which is a fast distributed key-value store.

6 comments
Add a comment