Search code examples
javascriptnode.jsinstagraminstagram-apiinstagram-graph-api

How to refresh Instagram Basic Display API token automatically?


I'm trying to set up an instagram feed (just images and links) of a public instagram account for my Nextjs app.

I know I need to use the Instagram Basic Display API and get a Long-Lived Access Token but it expires after 60 days and I don't want to have to manually refresh it. Does anyone know a good, preferably free, way of doing this automatically?

I have looked at instagram-token-agent but that setup uses Heroku and an add-on that costs $30 a month which seems high.

Any ideas or links would be really helpful, thanks!


Solution

  • I eventually ended up using Google Cloud Secret Manager.

    Overview: Secret Manager stores long-lived token and every rotation triggers a pub/sub that then triggers a cloud function. The cloud function refreshes the token for a new one and then adds a new version to the secret.

    Create New Secret

    Name it "instagram-token" and add your long lived token as the secret value. For now leave everything else default and create secret.

    Create a service account for secret manager

    In your terminal:

    gcloud auth login
    

    then

    gcloud beta services identity create --service "secretmanager.googleapis.com" --project "YOUR_GCP_PROJECT_ID"
    

    It may ask you to install gcloud beta commands.

    IMPORTANT: Make sure you note down the full name of the service account returned in the terminal. If you have lost it, run the same command again.

    Create pub/sub topic

    Create a new topic and name it "instagram-token-refresh", untick 'add a default subscription'.

    Give secret manager permission to publish pub/sub

    In your new pub/sub topic go to permissions -> Add Principle. Search and add the service account name added above. service-{id}@gcp-sa-secretmanager.iam.gserviceaccount.com. Add the new role Pub/Sub Publisher

    Add rotation and pub/sub to secret

    1. Go to your "instagram-token" secret and "edit secret".
    2. Rotation -> custom -> every 50 days
    3. Notifications -> Add Topic -> Select "instagram-token-refresh"
    4. Save

    Now every 50 days your "instagram-token-refresh" pub/sub will be triggered.

    Create Cloud Function

    1. Search cloud functions -> enable -> create cloud function
    2. Function name: "Refresh-Instagram-Token"
    3. Trigger: pub/sub -> Select "instagram-token-refresh"
    4. click next
    5. Entry Point: "refreshInstaToken"
    6. Edit files:

    You might need to enable to cloud build API

    package.json

    {
      "name": "refresh-instagram-token",
      "version": "0.0.1",
      "dependencies": {
        "@google-cloud/pubsub": "^0.18.0",
        "@google-cloud/secret-manager": "^3.10.1",
        "axios": "^0.24.0"
      }
    }

    index.js

    // Import the Secret Manager client
    const { SecretManagerServiceClient } = require("@google-cloud/secret-manager");
    const axios = require('axios');
    
    // name of function is the same as entry point
    exports.refreshInstaToken = async (event, context) => {
      // check pub/sub message is rotation to prevent infinte looping
      const event_type = event && event.attributes.eventType;
      //allowing SECRET_VERSION_ENABLE lets you manually trigger this function by disabling the secret and then enabling it (rather than waiting for rotation trigger)
      if (event_type != "SECRET_ROTATE" && event_type != "SECRET_VERSION_ENABLE") {
        return null;
      }
      // secret name
      const parent = event.attributes.secretId;
      const name = parent + "/versions/latest";
      // Instantiates a client
      const client = new SecretManagerServiceClient();
      // get latest secret
      const [version] = await client.accessSecretVersion({
        name: name,
      });
      // Extract the payload as a string.
      const secret = version.payload.data.toString();
    
      // refresh token
      const requesturl = `https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=${secret}`;
      const response = await axios.get(requesturl);
      const data = await response.data;
      // data = {"access_token", "token_type", "expires_in"}
    
      // check access_token isn't null
      if (data && data.access_token) {
        // Payload is the plaintext data to store in the secret
        const newSecret = Buffer.from(data.access_token, "utf8");
        // add new secret version (the refreshed token)
        const [newVersion] = await client.addSecretVersion({
          parent: parent,
          payload: {
            data: newSecret,
          },
        });
        console.log(`Added new secret version ${newVersion.name}`);
        // get new secret version number
        let newVersionN = newVersion.name.split("/");
        newVersionN = newVersionN[newVersionN.length - 1];
        if (newVersionN > 1) {
          // if is a second version delete one before it
          const nameToDestroy = parent + "/versions/" + (newVersionN - 1);
          const [deletedVersion] = await client.destroySecretVersion({
            name: nameToDestroy,
          });
          console.info(`Destroyed ${deletedVersion.name}`);
        }
      }
    };

    Adding/Accessing Secrets Ref

    Consume event notifications with Cloud Functions Ref

    Give cloud functions permissions to Secret

    1. Go to your secret -> permission
    2. Add -> {project-id}@appspot.gserviceaccount.com
    3. Add role "Secret Manager Admin"

    Accessing Secret Manager from service account

    1. Create new service account name "instagram-token".
    2. In new service account -> keys -> add keys -> save to desktop
    3. Go to your secret -> permission -> add -> "instagram-token...gserviceaccount.com" and give the role of "Secret Manager Secret Accessor"

    Setup credentials environment variable

    1. create .env.local file in next js root directory

    2. add new empty value GOOGLE_APPLICATION_CREDENTIALS=

    3. Convert JSON file to Base64 key and copy to clipboard MAC

      openssl base64 < /Users/{username}/Desktop/service-account.json | tr -d '\n' | pbcopy Convert JSON file to Base64 WINDOWS

    certutil -encode service-account.json encoded.txt

    1. paste to variable so you will have something like GOOGLE_APPLICATION_CREDENTIALS=faGdfdSytDsdcDg...

    Authenticating GCP in Next.js

    Install @google-cloud/secret-manager npm i @google-cloud/secret-manager

    const {
      SecretManagerServiceClient
    } = require("@google-cloud/secret-manager");
    
    export const getInstagramToken = async() => {
      // parse your base 64 env variable to a JSON object
      const credentials = JSON.parse(
        Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS, "base64").toString()
      );
    
      // TO DO -> CHANGE
      const projectId = "eleanor-daisy";
      const secretId = "instagram-token";
    
      // set up credentials config
      const config = {
        projectId,
        credentials,
      };
    
      // init secret manager with credentials
      const client = new SecretManagerServiceClient(config);
    
      const secretName = `projects/${projectId}/secrets/${secretId}/versions/latest`;
    
      // Access the secret.
      const [accessResponse] = await client.accessSecretVersion({
        name: secretName,
      });
    
      const instaToken = accessResponse.payload.data.toString("utf8");
    
      return instaToken;
    };

    Add GOOGLE_APPLICATION_CREDENTIALS and key to vercel when deploying.

    Done! I might make a video tutorial on this as there's not much out there, let me know if that would be helpful :)