Getting 401 while using external oauth providers in Event handler

Hello Experts,

I have configured Jira’s external OAuth with SurveySparrow. The app consists of three modules: Project Page, Issue Context, and Issue Panel. I have successfully initiated OAuth on the Project Page, and it works as expected.

Additionally, I have added an event handler to listen for Jira events. Based on these events, I need to make an API call to the SurveySparrow provider. However, when I attempt to make the API call, it throws a 401 Unauthorized error.

I would like to know if it is possible to use an external OAuth provider to make API calls inside an event handler. If yes, how can I ensure proper authentication for these calls?

Could someone please help me resolve this issue or provide guidance on the best approach to handle external OAuth for making API calls in this scenario?

Thank you in advance!
FYI Adding the event Handler :

// src/eventHandler.js
import api, { storage, route } from '@forge/api';

const STRINGS = {

  EVENT_TYPES: {
    ISSUE_CREATED: "created",
    ISSUE_UPDATED: "updated",
    ISSUE_DELETED: "deleted"
  },
  PROVIDERS: {
    SURVEYSPARROW: "surveysparrow",
    SURVEYSPARROW_API: "surveysparrow-api",
  },
  JIRA_EVENTS: {
    CREATED: "avi:jira:created:issue",
    UPDATED: "avi:jira:updated:issue",
    DELETED: "avi:jira:deleted:issue"
  },
  API: {
    CHANNELS: "/3/channels",
    CONTENT_TYPE: "application/json",
    CONTENT_TYPE_KEY: "Content-Type",
    METHODS: {
      PUT: "PUT",
      GET: "GET"
    }
  },
  STORAGE_KEYS: {
    DASHBOARD_DATA: "DashboardData",
    TRIGGER: "Trigger",
    TOKEN: "Token"
  },
};

const getIssueDetails = async (issueKey) => {
  try {
    const response = await api.asApp().requestJira(route`/rest/api/3/issue/${issueKey}`, {
      method: STRINGS.API.METHODS.GET,
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch issue details: ${response.status} ${response.statusText}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Error fetching issue details:", error);
    throw error;
  }
};

const isMatchedTrigger = (events, type) => {
  return events.some(event => event.value.includes(type))
}
const  IsConditionSatisfied = async (details, conditions) => {
  const { fields } = details;
  for (const condition of conditions?.childConditions) {
      const { leftOperand, rightOperand, operator } = condition;
      const fieldValue = leftOperand?.value.includes('.')
          ? leftOperand.value.split('.').reduce((obj, key) => obj?.[key], fields)
          : fields[leftOperand.value];
      const isConditionMet = (() => {
          switch (operator.value) {
              case "is_equal_to":
                  return fieldValue === rightOperand?.value;
              case "not_equal_to":
                  return fieldValue !== rightOperand?.value;
              case "contains":
                  return Array.isArray(fieldValue) && fieldValue.includes(rightOperand?.value);
              case "not_contains":
                  return Array.isArray(fieldValue) && !fieldValue.includes(rightOperand?.value);
              default:
                  return false; 
          }
      })();
      if (!isConditionMet) {
          return false;
      }
  }
  return true;
}
const surveysparrowFetch = async (url,options) => {
  const surveysparrow = api
  .asUser()
  .withProvider(
    STRINGS.PROVIDERS.SURVEYSPARROW,
    STRINGS.PROVIDERS.SURVEYSPARROW_API
  );
  if(surveysparrow.hasCredentials()){
    console.log("unauthorized !")
    await surveysparrow.requestCredentials()
  } else {
    return await surveysparrow.fetch(url, options)
  }
}



const handleEvent = async (details, type, name) => {
  try {
    const triggers = await storage.get(STRINGS.STORAGE_KEYS.TRIGGER)
    let validTriggers = []
    await triggers.map((trigger)=> {
      if(isMatchedTrigger(trigger?.events, type)){
        validTriggers = [...validTriggers, trigger]
      }
    })
    if(validTriggers.length){
      await validTriggers?.map(async (validTirgger) => {
        const isValid = IsConditionSatisfied(details, validTirgger?.conditions)
        if(isValid){
          const payload = {
            survey_id: validTirgger?.survey?.value,
            contacts: [
              {
                email: "heisenbergbb1729@gmail.com"
              }
            ]
          }
          const options = {
            method: STRINGS.API.METHODS.PUT,
            headers: {
              [STRINGS.API.CONTENT_TYPE_KEY] : STRINGS.API.CONTENT_TYPE
            },
            body: JSON.stringify(payload)
          }
          await surveysparrowFetch(`${STRINGS.API.CHANNELS}/${validTirgger?.shareChannel?.value}`, options)
        }
      })
    }
    return true

  } catch (error) {
    console.error('Trigger Event Error', error);
    throw error;
  }
};

export async function handler(event, context) {
  const { 
    eventType, 
    issue: { fields: { issuetype: { name } } }
  } = event;
  
  const details = await getIssueDetails(event?.issue?.key)
  const eventMap = {
    [STRINGS.JIRA_EVENTS.CREATED]: STRINGS.EVENT_TYPES.ISSUE_CREATED,
    [STRINGS.JIRA_EVENTS.UPDATED]: STRINGS.EVENT_TYPES.ISSUE_UPDATED,
    [STRINGS.JIRA_EVENTS.DELETED]: STRINGS.EVENT_TYPES.ISSUE_DELETED
  };

  const eventTypeMapping = eventMap[eventType];
  if (eventTypeMapping) {
    return await handleEvent(details, eventTypeMapping, name);
  }
  
  console.log(`Unhandled event type: ${eventType}`);
}

And the error is

INFO    15:51:32.199  aeaec663-6a3f-4019-b9fe-df4be9913d70  unauthorized !
INFO    15:51:32.200  aeaec663-6a3f-4019-b9fe-df4be9913d70  unauthorized !
ERROR   15:51:32.219  aeaec663-6a3f-4019-b9fe-df4be9913d70  UnhandledPromiseRejection with reason: Authentication Required
ERROR   15:51:32.220  aeaec663-6a3f-4019-b9fe-df4be9913d70  UnhandledPromiseRejection with reason: Authentication Required

@BoZhang please help on this case

Hi @HeisenbergBB

Unfortunately this is not possible. The reason is because the events are triggered from the backend where there is no user context available and external OAuth connections are linked to users.

1 Like

Thanks for the quick update @BoZhang . Can we able to get token from the oauth provider and store it in app properties or storage ? . or is it possile to get headers in profile retriver function ?
can you please help on this case. Thanks in advance :raised_hands:

import { AuthProfile } from '@forge/response';
import { storage } from "@forge/api";
import { SPARROW_ICON_URL, DEFAULT_STORAGE_KEYS } from "./common/constants";

const setAuthStatus = async (status) => 
  storage.set(DEFAULT_STORAGE_KEYS.IS_AUTHENTICATED, status);
const setProfileInfo = async (info) => 
  storage.set(DEFAULT_STORAGE_KEYS.PROFILE_INFO, info);

export const retriever = async ({ status, body: externalProfile }) => {
  if (status !== 200) {
    throw new Error(`${status}`);
  }

  await setAuthStatus(true);
  const currentUser = externalProfile?.data?.find(user => user.current_user);
  await setProfileInfo(currentUser);
  return new AuthProfile({
    id: String(currentUser?.id),
    displayName: currentUser?.name || currentUser?.email,
    avatarUrl: currentUser?.profile_pic || SPARROW_ICON_URL,
  });
}

Unfortunately, the OAuth token is not exposed to your app, there is no way around this today. We have a feature request open which sounds like what you need, I would recommend you to upvote it. In the meantime, the only work around is to somehow get the end user to trigger an interaction so that there is user context available with the invocation.

Thanks for the update @BoZhang :raised_hands: