Authentication for Social Media

As you may see on our social media accounts (LinkedIn, X, and Instagram at the moment), we not only generate news articles twice a day, but also distribute them shortly after across various channels. On LinkedIn, we show you todays most important headlines, and on X and Instagram, you get a one minute video narrating todays top news.

Of course, we could open up the apps in questions twice a day and share it by copy-pasting every day. But, we already have a pipeline in place to do that for us, so we leverage that instead. However, for the sake of sharing content, we need the correct access rights.

The basics for authenticating

Almost every popular social media platform available today has some form of API available to let you share content programmatically, for which you can sign up as an application. Another thing they have in common: some form of authenticating mechanism to validate that it is really you posting.

For some platforms, for example for X, this is rather straightforward. You sign up as an application, get assigned a client ID and secret, and send that along with all subsequent requests, for example, once you want to send out a tweet.

For others, operations like sharing something is tied more strongly to individual accounts. Hence, each platform (ideally) offers some way of authenticating via your applications account, for example via OAuth. To use it, we trigger a manual login, store the most long-lived token that we get as a result, and store it together with their time of expiration.

Usually, that means we have two logical parts we need to cover: the redirect to a login page of the social media platform, and a callback endpoint to receive the results and store it away for us to use later.

We built both into our Social component. To do so, we dynamically load authentication logic per platform as so-called “authentication connectors”. This way, they are mounted as endpoints into the API of our component, and we can add new connectors by just adding a file later down the line:


export const initAuthenticationConnector = async () => {
    const authenticationProvider = '/authentication';
    const files = fs.readdirSync(authenticationProvider);
    for (const file of files) {
        if (path.extname(file) === '.js') {
            const module = await import(`${authenticationProvider}/${file}`);
            authenticationConnectors[module.name] = {
                authUrl: module.authUrl,
                name: module.name,
                handleRedirect: module.handleRedirect,
                getExpirationDate: module.getExpirationDate,
                getToken: module.getToken,
            };
        }
    }
    return authenticationConnectors;
};
                

What we do here is fetch all JavaScript files that concern themselves with authentication, and have them available as imports, to have them expose the authentication/login URL, to handle the redirects coming from that, and to later make the long-lived tokens available to the rest of our pipeline.

Practical example: LinkedIn

Let's take a look what such a connector can look like, taking LinkedIn as an example. LinkedIn uses a procedure similar to OAuth2. Hence, the first thing we need is the login URL that the LinkedIn endpoint in our social component should send us to:


export const authUrl = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${STATE}&scope=${encodeURIComponent(SCOPE)}`;
                

With it, we send our client ID (which we get by signing up as an application on LinkedIn), the redirect URL (which is the other endpoint we mount in our component), a state to prevent cross-site attacks, and the scope that we would like to authenticate with. The latter is dependent on the platform, on LinkedIn that scope entails our ability to post to our own profile.

Visiting the endpoint created from this redirects us to the LinkedIn login page:

Whatever account we log in with here, we can start sending posts to. Hence, we log in with the Daily-Digest account. This takes us to the other endpoint of the social component, where we invoke the following:


const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
        code,
        grant_type: 'authorization_code',
        redirect_uri: redirectUrl,
    }),
});
                

You may notice that we use a “code” variable in the request above. This is something that LinkedIn supplies to our redirect endpoint. With it and the request, we can query for a long-lived access token, that we can later use to make other requests to the platform. Each platform handles this differently, but in the end we (usually) receive a token, and a time until that token is valid. We store both in our database.

The authentication clock is ticking

Given all of the above, we now have a token we can use to send out requests to the platforms we have implemented. However, as mentioned, these are only valid for a short time (depending on the platform, between a few weeks and a year). We do not want to end up having to fix this after we run out of time, so we built the following:


export const notifyOnExpiringAuthenticationProvider = async (supabase) => {
    const now = Date.now();
    for (const provider of Object.values(authenticationConnectors)) {
        const expirationDate = await provider.getExpirationDate(supabase);
        if (expirationDate != null && (expirationDate - RE_AUTHENTICATION_NOTIFICATION_DAYS) < now) {
            await sendNotification({
                message: `Refresh Token for ${provider.name} expires on ${new Date(expirationDate).toLocaleString()}`,
                priority: 'high'
            });`);
        }
    }
}
                

We determine if, for each platform, the time until their expiration time is less than a threshold set by us, and if it is, we send out a notification to all our developers to take action. As we have the aforementioned endpoints in place, all we need to do then is log in one more time.

This way, we always have access to our social media accounts at our disposal, right from within our pipeline. We will jump into the topic of actually sharing to these platforms at another time, however, this forms the baseline of delivering news to your social media feeds twice a day!