1 Dec 2023

Maintaining Refresh Tokens with MongoDB Triggers

trigger diagram

Introduction

You may need a way to keep using refresh tokens in server-to-server communications or any workflow where it doesn't make sense to have the user going through the login process each hour. That's the purpose of this blog post. We'll share a way to maintain your refresh tokens valid in MongoDB Atlas through Triggers scheduled every week (or any other period of your preference lower than 14 days).

I recommend you go through this blog post as a complement to this reading.

To summarize, what you need to know is:

1. Refresh tokens are used to obtain new three-legged tokens without the need for the user to go through the login process once more

2. Refresh tokens are valid for 14 days.

With that in mind, we can deep dive into the details of this implementation.

The approach

This approach is based on MongoDB Atlas leveraging its trigger capabilities.

For that to work, you'll need to store your user's tokens in a database and a collection as individual documents.

For this sample, we created documents with the format below:

{
  "_id":{"$oid":"Automatically assigned"},
  "userId":"USER ID",
  "access_token":"USER TOKEN",
  "refresh_token":"USER REFRESH TOKEN",
  "email":"USER EMAIL"
}

Once your user's tokens are stored in this way, you can start configuring your trigger.

This blog covers Scheduled Triggers based on Atlas functions.

You'll need to use the code below in your function, adjusting with your own scopes, credentials header, cluster name, database name, and collection name. 

const request = require('request');

// wrap a request in an promise
function refreshToken(refresh_token) {
  console.log('Refreshing token...');
  return new Promise((resolve, reject) => {
    const options = {
      method: 'POST',
      url: 'https://developer.api.autodesk.com/authentication/v2/token',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Accept: 'application/json',
        Authorization: 'Basic <<ADD YOUR CREDENTIALS HERE>>'
      },
      form: {
        grant_type: 'refresh_token',
        refresh_token: refresh_token,
        scope: 'data:read'
      }
    };
    request(options, async function (error, response, body) {
      if (error) throw new Error(error);
    
      console.log(body);
      var jsonBody = JSON.parse(body);
      resolve(jsonBody);
    });
  });
}

exports = async function() {
  /*
    A Scheduled Trigger will always call a function without arguments.
    Documentation on Triggers: https://www.mongodb.com/docs/atlas/app-services/triggers/overview/
    Functions run by Triggers are run as System users and have full access to Services, Functions, and MongoDB Data.
    Access a mongodb service:
    const collection = context.services.get(<SERVICE_NAME>).db("db_name").collection("coll_name");
    const doc = collection.findOne({ name: "mongodb" });
    Note: In Atlas Triggers, the service name is defaulted to the cluster name.
    Call other named functions if they are defined in your application:
    const result = context.functions.execute("function_name", arg1, arg2);
    Access the default http client and execute a GET request:
    const response = context.http.get({ url: <URL> })
    Learn more about http client here: https://www.mongodb.com/docs/atlas/app-services/functions/context/#std-label-context-http
  */
  
  const tokens = context.services.get("<<YOUR CLUSTER NAME>>").db("<<YOUR DB NAME>>").collection("<<YOUR COLLECTION NAME>>");;

  const tokensCursor = tokens.find();
  
  for await (const token of tokensCursor) {
    console.log(`Refreshing ${token.email} token...`);

    let new_refresh_token;
    let new_access_token;
    
    var jsonBody = await refreshToken(token.refresh_token);
    new_refresh_token = jsonBody.refresh_token;
    new_access_token = jsonBody.access_token;
    
    console.log('email',token.email );
    console.log('new access token', new_access_token);
    console.log('new refresh token', new_refresh_token);
    const filter = { email: token.email };
      
    const updateDoc = {
      $set: {
        access_token: new_access_token,
        refresh_token: new_refresh_token
      },
    };
    
    const updateOptions = { upsert: false };
    
    // Update the first document that matches the filter
    const result = await tokens.updateOne(filter, updateDoc, updateOptions);

  }
};

Refer to the video below for the trigger configuration in details:

 

Known Limitations

You might have trouble escalating this solution as the Atlas function is limited to 300 seconds.

If you manage hundreds of user's tokens in a single collection, you might get a timeout error with this approach.

Possible solutions are:

1. Split your users into multiple collections and configure a trigger for each one of them

2. Try sending events to AWS EventBridge

 

Please let us know if you find another solution or improvement for this implementation.

Related Article