Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUERY] External auth for non-link events #401

Open
radvansky-tomas opened this issue Jan 2, 2025 · 9 comments
Open

[QUERY] External auth for non-link events #401

radvansky-tomas opened this issue Jan 2, 2025 · 9 comments
Labels
question Further information is requested

Comments

@radvansky-tomas
Copy link

I am trying to use external auth for non link events such as mentions, app mentions etc.
I saw example here:
https://api.slack.com/tutorials/tracks/oauth-tutorial

But this is using some form of hack method when instead of using external auth, they / you are creating link and then executing another workflow after. Which seem like massive pain.

I could probably handle whole oauth flow, but client.apps.auth.external.get does not expose refresh token, so manual refresh is not doable. I guess I can make custom webhook to capture whole oauth flow.

But before I do, that Id like to know, if there is really no way for me just running reaction events with oauth in place, with automatic token refresh mechanism, without constant recreation of links back/forth.

@zimeg zimeg added the question Further information is requested label Jan 2, 2025
@zimeg
Copy link
Member

zimeg commented Jan 2, 2025

Hey @radvansky-tomas 👋 Thanks for the inquiry!

The tutorial you linked shows external authentication as part of a workflow that starts from a link trigger, but that workflow could start from other triggers, like the reaction_added event.

This has limitations around the ordering of steps - only link triggers contain interactivity when starting - but external authentication remains possible. Either DEVELOPER or END_USER credential source can be used, with END_USER needing interactivity to determine who's running the workflow. Interactivity can also be found after a workflow has started from step outputs that contain it, like from send_message.

With all of this, token rotation is taken care of for you!

Let me know if setting up a workflow with one of these options works or if more detail is needed, I'm here to help 🙏

@radvansky-tomas
Copy link
Author

Sure thank you for reply, yes I tried all that. However I seem to not understand need for link trigger when END_USER is used.

So lets consider simple use case. You want to do external auth with your google sheets stuff. You do whatever is required to initialise workflow (my understanding is link shortcut) that will open that small banner, with run Workflow, then you connect via oAuth.

But what next ?!

It seems that every other event I am interested in:

  • message sent
  • reactions
  • reply in threads etc

Does NOT support END_USER, and only workaround I found was to "create" another link trigger after, and then run another workflow. That makes no sense to me.

Now I am actually trying to do complete oAuth flow without using external auth approach (via navigating to urls + webhooks and storing tokens / doing refresh in datastore) so at least I will have some control over it.

@zimeg
Copy link
Member

zimeg commented Jan 2, 2025

@radvansky-tomas Hmm, I'm not sure that those events restrict END_USER usage, but at some point I do believe a button or link will need to be pressed before the step using external authentication can run. This speedbump was added to decide which user accounts to use in the step that has external authentication.

In an implemented workflow, that might resemble:

  1. Event trigger: Starts the workflow after an :eyes: reaction is added
  2. Send a DM: Include interactive blocks that "gate" the next steps
  3. Add a row to the Google Sheet: Make use of apps.auth.external.get

This setup requires another change to make use of END_USER where the last interactor needs to be specified:

  const step = ExampleWorkflow.addStep(definition, {
    providerAccessTokenId: {
      credential_source: "END_USER",
+     user_context_source: "LAST_INTERACTOR",
    },
  });

This isn't documented on https://api.slack.com yet, but we're working on updating this.

Here's an example button too
ExampleWorkflow.addStep(Schema.slack.functions.SendDm, {
  user_id: EventTriggerContextData.ReactionAdded.user_id,
  message: "Proceed with the next step",
  interactive_blocks: [
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: {
            type: "plain_text",
            text: "Click Me",
            emoji: true,
          },
        },
      ],
    },
  ],
});

I understand that this setup requires additional steps to complete each workflow, but I'm hoping it can still work so the advantages of token management with external authentication can be used! Let me know if this works for you or not 🙏

@radvansky-tomas
Copy link
Author

As I explained, I fully understand that for initialising oauth flow you need Link Trigger that I have and it works.
However I dont understand why for subsequent events, where its clear if they are coming from user, it would require link trigger.

https://api.slack.com/automation/external-auth#end-user-tokens

If you would like the workflow to use the account of the end user running the workflow, use credential_source: "END_USER".

The end user will be asked to authenticate with the external service in order to connect and grant Slack access to their account before running the workflow. This workflow can only be started by a [Link Trigger](https://api.slack.com/automation/triggers/link), as this is the only type of trigger guaranteed to originate directly from an end-user interaction.

I didnt know about user_context_source: "LAST_INTERACTOR" Will give it a shot.

Basically I want to see some example of:
TriggerEventTypes.ReactionAdded
TriggerEventTypes.MessagePosted
TriggerEventTypes.AppMentioned

With direct use of END_USER context

@radvansky-tomas
Copy link
Author

So I have this:

import { Trigger } from "deno-slack-api/types.ts";
import { TriggerEventTypes, TriggerTypes } from "deno-slack-api/mod.ts";
import workflow from "../workflows/reaction_added_oauth_workflow.ts";

const trigger: Trigger<typeof workflow.definition> = {
  type: TriggerTypes.Event,
  name: "Reaction handler",
  description: "Processes reactions",
  workflow: "#/workflows/reaction-added-oauth-workflow",
  event: {
    event_type: TriggerEventTypes.ReactionAdded,
    channel_ids: ["C086K2H9FSS"],
    filter: {
      version: 1,
      root: {
        operator: "AND",
        inputs: [{
          statement: "{{data.reaction}} == 'file_cabinet'",
        }],
      },
    },
  },
  inputs: {
    channel_id: { value: "{{data.channel_id}}" },
    message_ts: { value: "{{data.message_ts}}" },
    text: { value: "{{data.reaction}}" },
    reactor_id: { value: "{{data.user_id}}" },
  },
};

export default trigger;

And then:

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { def as getExternalToken } from "../functions/get_external_token.ts";
import { def as processAudio } from "../functions/process_audio.ts";
import { GetMessagePermalinkFunctionDefinition } from "../functions/get_message_permalink.ts";

const workflow = DefineWorkflow({
  callback_id: "reaction-added-oauth-workflow",
  title: "Reaction Added OAuth Workflow",

  input_parameters: {
    properties: {
      channel_id: {
        type: Schema.slack.types.channel_id,
        description: "The channel containing the app mention",
      },
      message_ts: {
        type: Schema.slack.types.message_ts,
        description: "Message timestamp of the app mention",
      },
      text: { type: Schema.types.string },
      reactor_id: {
        type: Schema.slack.types.user_id,
        description: "User that added the app mention",
      },
    },
    required: ["channel_id", "message_ts", "text", "reactor_id"],
  },
});

const externalToken = workflow.addStep(getExternalToken, {
  channel_id: workflow.inputs.channel_id,
  message_ts: workflow.inputs.message_ts,
  reactor_access_token_id: {
    credential_source: "END_USER",
    user_context_source: "LAST_INTERACTOR",
  },
});

And the moment I add any step with credential_source: "END_USER",
It will stop working, with not error etc...it will just not get called

I've added LAST_INTERACTOR, not difference

@zimeg
Copy link
Member

zimeg commented Jan 3, 2025

@radvansky-tomas Thanks for sharing this example - this is great reference! 📚

However I dont understand why for subsequent events, where its clear if they are coming from user, it would require link trigger.

This is all super good feedback! I agree that some events like reaction_added or message_posted often originate from a user, but these can also be done by bots 🤖 For that reason, interactivity isn't included in event, scheduled, or webhook triggers.

I do also want to note that "link triggers" are different than message buttons. A link trigger starts a workflow, while message buttons can be included in messages and generate interactivity once clicked.

I've added LAST_INTERACTOR, not difference

The documentation right now isn't so clear on this, but including a message button before the external token step will generate interactivity needed for the LAST_INTERACTOR:

workflow.addStep(Schema.slack.functions.SendDm, {
  user_id: EventTriggerContextData.ReactionAdded.user_id,
  message: "Proceed with the next step",
  interactive_blocks: [
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: {
            type: "plain_text",
            text: "Click Me",
            emoji: true,
          },
        },
      ],
    },
  ],
});

Please let me know if adding this step starts your workflow again!

@radvansky-tomas
Copy link
Author

Thank you for reply, but this is even worse than link trigger. I understand that "sometimes" event might not be triggered by actual user. But i dont see point why you cant just pass NULL there OR empty access token. Rather then dont execute trigger itself.

As I said, use case is simple:

  • do External auth (doesnt matter how), for now its just running workflow via App_Mentioned. That works fine
  • It should store and refresh access token automatically (which apparently works)
  • but then how to actually use it?!

Whats the point of all that, if for every usable action, I have to start another workflow, click on some buttons and start another workflow ?!

Now when you have valid access token connected to UserID, there should be a way to just "get it"
And events, should be triggered ALWAYS. But based on knowledge populate fields.
If you are not certain about user (?bot) then just keep it null

Because now flow will look like this:

User: Hello @AppMention, do something magical
System: <Banner> press <Button>

Now full modal appears, do oAuth.
User: Funny message
User: <adding emoji>
System: <Banner> did you really add emoji, press <button> if it wasnt bot?!

Now full modal, <Button> run workflow

And that second part will happen EVERY SINGLE TIME.
I wanted to link emoji add to use case similar to your example:
https://github.com/slack-samples/deno-message-translator

But if you think about this from user perspective (not global oauth)
Then every single message translation would invoke additional modal + button click

@radvansky-tomas
Copy link
Author

Update, yeah subsequent button presses does NOT invoke another modal, thats better. OK
Will try to do full example and post full integration here, so everyone can benefit from it ;)

@zimeg
Copy link
Member

zimeg commented Jan 6, 2025

I understand that "sometimes" event might not be triggered by actual user. But i dont see point why you cant just pass NULL there OR empty access token. Rather then dont execute trigger itself.

@radvansky-tomas I've been caught on this myself and am sharing the current reasoning for the product decision. Sharing the use case you're finding with /feedback in a channel or sending an email to [email protected] helps our product teams learn what might need adjusting 🙏

Update, yeah subsequent button presses does NOT invoke another modal, thats better.

Glad to know this is a bit smoother, even if not so ideal. Please feel free to share examples or follow up with other questions whenever! 👾

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants