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

Installation flow fails with slack_oauth_invalid_state when running an app on Firebase Hosting #1730

Closed
4 of 10 tasks
multimanic opened this issue Jan 31, 2023 · 9 comments
Closed
4 of 10 tasks
Labels
question M-T: User needs support to use the project

Comments

@multimanic
Copy link

multimanic commented Jan 31, 2023

Description

Describe your issue here.
I'm using /slack/install to install my distributed app to a test workspace. Everything works perfectly fine using ngrok but as soon as I deploy my app to my server the installation fails with the page /slack/oauth_redirect?code=... saying

Oops, Something Went Wrong!
Please try again or contact the app owner (reason: slack_oauth_invalid_state)

The server debug log says

[ERROR]  OAuth:InstallProvider:0 Error: The state parameter is not for this browser session.

My code is pretty much like this by @seratch but using Firestore as database (again, everything works perfectly fine with ngrok).

What type of issue is this? (place an x in one of the [ ])

  • bug
  • enhancement (feature request)
  • question
  • documentation related
  • example code related
  • testing related
  • discussion

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

Bug Report

Filling out the following details about bugs will help us solve your issue sooner.

Reproducible in:

package version: @slack/bolt 3.12.2 and @slack/oauth 2.6.0

node version: 18

OS version(s): Google Firebase

Steps to reproduce:

  1. Build a bolt app like so but use Firebase as installation store
  2. open https://de29-...-8e61.ngrok.io/slack/install and go through the install flow. The flow succeeds with the page https://de29-...-8e61.ngrok.io/slack/oauth_redirect?code=15... saying

Thank you!
Redirecting to the Slack App... click [here]. If you use the browser version of Slack, click this link instead.

  1. deploy the same app to Firebase using Firebase Hosting
  2. open https:///slack/install and go through the install flow. The flow ends with the page https:///slack/oauth_redirect?code=15... saying

Oops, Something Went Wrong!
Please try again or contact the app owner (reason: slack_oauth_invalid_state)

Expected result:

Installation should succeed.

Actual result:

Installation flow fails with slack_oauth_invalid_state and a debug log
[ERROR] OAuth:InstallProvider:0 Error: The state parameter is not for this browser session.

Attachments:

Logs, screenshots, screencast, sample project, funny gif, etc.

@seratch seratch added the question M-T: User needs support to use the project label Jan 31, 2023
@seratch
Copy link
Member

seratch commented Jan 31, 2023

Hi @multimanic, thanks for asking the question!

We don't officially support the runtime, so we are not able to help you out on how to deploy apps onto Firebase Hosting. That aside, possible causes of your issue can be:

  • The domain for your /slack/install and /slack/oauth_redirect may not be the same. In this case, the state parameter validation fails as it relies on 1st party cookie to transfer the state through the redirections between slack.com and your site.
  • I don't know well about Firebase Hosting but if the hosting service does not allow usual set-cookie response headers like the OAuth package does, you may have to disable the state verification (it's not great for security though) otherwise, implement your own OAuth flow handlers.

I hope this was helpful to you.

@multimanic
Copy link
Author

Thank you so much @seratch for unblocking me after 4 days of desperation. I also had a hunch that the state verification is the issue. However, I can see all the cookies set and sent on firebase like on ngrok.
Deactivating the state verification solved the problem. Can you elaborate in the security risks?
And is the StateStore and alternative to the cookie-based state verification? If so, is there any sample code? I couldn't find any. I think Firestore would be a simple StateStore, right?

@seratch seratch changed the title Installation flow fails with slack_oauth_invalid_state Installation flow fails with slack_oauth_invalid_state when running an app on Firebase Hosting Jan 31, 2023
@seratch
Copy link
Member

seratch commented Jan 31, 2023

@multimanic

Deactivating the state verification solved the problem.

First off, the ideal solution is identifying the cause of your situation with Firebase and keeping the state verification on. Generally speaking, I don't recommend disabling it. With that being said, if you think the risk is acceptable for your app after going through this reply, it's your decision.

Can you elaborate in the security risks?

The security risk is not specific to Slack's OAuth flow. It is a general CSRF attack risk for your end users who install your app. Refer to the following resources:

As mentioned in the above resources, when an attacker navigates your end user to a redirect URL with the attacker's auth code parameter and a valid state, your end user may complete their OAuth flow with an unexpected association. So, verifying the state through the end-to-end OAuth flow is a highly recommended security measure for apps providing OAuth flows. That's why our SDKs enable the verification by default.

As long as you are aware of the risk with the lack of state parameter, you can go ahead with disabling it. If you don't offer any account mapping feature like associating a Slack workspace/user with your external service account, the risk should be quite limited. Otherwise, we highly recommend implementing something that ensures the same user visiting both /slack/install and /slack/oauth_redirect in an OAuth flow. For this, the most common way should be managing the user's state by setting browser cookies like our library does.

And is the StateStore and alternative to the cookie-based state verification? If so, is there any sample code? I couldn't find any. I think Firestore would be a simple StateStore, right?

No, actually. StateStore provides the abstraction of the server-side state value storage, and it does not offer any functionality to manage browser cookies. Just having a server-side state store is not a complete solution as the security measure. If an attacker issues their state parameter on their side plus share the authorize URL with the generated state parameter with their target user agent, the attack still can succeed (because the state parameter value is not yet consumed on the server-side). Since the past implementation was not secure enough this way, I've resolved the potential risk at slackapi/node-slack-sdk#1435.

I hope this comment answers your questions. If everything is clear now, would you mind closing this issue?

@multimanic
Copy link
Author

multimanic commented Feb 1, 2023

Thanks @seratch and I agree, I should fix this.

Here is my understanding of the flow ad the code:

  1. the page https://<my-host>/slack/install generates a code for the "state" and sets a cookie called slack-app-oauth-state. The page alos displays a button which points the browser to a page on https://slack.com/oauth/... and includes a URL parameter state which has the same state value.
  2. This URL parameter is carried through the OAuth flow ending with the page https://<my-host>/slack/oauth_redirect being loaded. This page also reads the cookie called slack-app-oauth-state and compares it with the URL parameter state. If they are not equal the InvalidStateError is thrown.

That said, I tried to compare the network trace and the cookies of my ngrok and my host. They are exactly the same.

  1. the page https://<my-host>/slack/install includes a response header:
set-cookie: slack-app-oauth-state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsT3B0aW9ucyI6eyJzY29wZXMiOlsiYXBwX21lbnRpb25zOnJlYWQiLCJjaGF0OndyaXRlIiwiY2hhdDp3cml0ZS5wdWJsaWMiLCJjb21tYW5kcyIsImdyb3Vwczp3cml0ZSIsImltOndyaXRlIiwibXBpbTp3cml0ZSIsInVzZXJzOnJlYWQuZW1haWwiLCJ1c2VyczpyZWFkIl19LCJub3ciOiIyMDIzLTAxLTMxVDIxOjM3OjE3LjIwMVoiLCJyYW5kb20iOjY2OTMxMSwiaWF0IjoxNjc1MjAxMDM3fQ.jFYu4tWbqB7uREQba28W3u-zBrfxVTpwI_GjtThqwCM; Path=/; Max-Age=600; HttpOnly; Secure

followed by this sequence

https://slack.com/oauth/v2/authorize?scope=app_mentions:read,chat:write,chat:write.public,commands,groups:write,im:write,mpim:write,users:read.email,users:read&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsT3B0aW9ucyI6eyJzY29wZXMiOlsiYXBwX21lbnRpb25zOnJlYWQiLCJjaGF0OndyaXRlIiwiY2hhdDp3cml0ZS5wdWJsaWMiLCJjb21tYW5kcyIsImdyb3Vwczp3cml0ZSIsImltOndyaXRlIiwibXBpbTp3cml0ZSIsInVzZXJzOnJlYWQuZW1haWwiLCJ1c2VyczpyZWFkIl19LCJub3ciOiIyMDIzLTAxLTMxVDIxOjM3OjE3LjIwMVoiLCJyYW5kb20iOjY2OTMxMSwiaWF0IjoxNjc1MjAxMDM3fQ.jFYu4tWbqB7uREQba28W3u-zBrfxVTpwI_GjtThqwCM&client_id=<client-ID>
https://slack.com/oauth/v2/authorize?scope=app_mentions:read,chat:write,chat:write.public,commands,groups:write,im:write,mpim:write,users:read.email,users:read&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsT3B0aW9ucyI6eyJzY29wZXMiOlsiYXBwX21lbnRpb25zOnJlYWQiLCJjaGF0OndyaXRlIiwiY2hhdDp3cml0ZS5wdWJsaWMiLCJjb21tYW5kcyIsImdyb3Vwczp3cml0ZSIsImltOndyaXRlIiwibXBpbTp3cml0ZSIsInVzZXJzOnJlYWQuZW1haWwiLCJ1c2VyczpyZWFkIl19LCJub3ciOiIyMDIzLTAxLTMxVDIxOjM3OjE3LjIwMVoiLCJyYW5kb20iOjY2OTMxMSwiaWF0IjoxNjc1MjAxMDM3fQ.jFYu4tWbqB7uREQba28W3u-zBrfxVTpwI_GjtThqwCM&client_id=<client-ID>&tracked=1
https://<my-workspace>.slack.com/oauth?client_id=<client-ID>&scope=app_mentions:read,chat:write,chat:write.public,commands,groups:write,im:write,mpim:write,users:read.email,users:read&user_scope=&redirect_uri=&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsT3B0aW9ucyI6eyJzY29wZXMiOlsiYXBwX21lbnRpb25zOnJlYWQiLCJjaGF0OndyaXRlIiwiY2hhdDp3cml0ZS5wdWJsaWMiLCJjb21tYW5kcyIsImdyb3Vwczp3cml0ZSIsImltOndyaXRlIiwibXBpbTp3cml0ZSIsInVzZXJzOnJlYWQuZW1haWwiLCJ1c2VyczpyZWFkIl19LCJub3ciOiIyMDIzLTAxLTMxVDIxOjM3OjE3LjIwMVoiLCJyYW5kb20iOjY2OTMxMSwiaWF0IjoxNjc1MjAxMDM3fQ.jFYu4tWbqB7uREQba28W3u-zBrfxVTpwI_GjtThqwCM&granular_bot_scope=1&single_channel=0&install_redirect=&tracked=1&team=

finally the page

https://<my-host>/slack/oauth_redirect?code=1509222955907.4725617052005.1b74ee7e3308fce9c3e5ed7c3a22ad124245d29098bf3921a08b1ba0e547ac63&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsT3B0aW9ucyI6eyJzY29wZXMiOlsiYXBwX21lbnRpb25zOnJlYWQiLCJjaGF0OndyaXRlIiwiY2hhdDp3cml0ZS5wdWJsaWMiLCJjb21tYW5kcyIsImdyb3Vwczp3cml0ZSIsImltOndyaXRlIiwibXBpbTp3cml0ZSIsInVzZXJzOnJlYWQuZW1haWwiLCJ1c2VyczpyZWFkIl19LCJub3ciOiIyMDIzLTAxLTMxVDIxOjM3OjE3LjIwMVoiLCJyYW5kb20iOjY2OTMxMSwiaWF0IjoxNjc1MjAxMDM3fQ.jFYu4tWbqB7uREQba28W3u-zBrfxVTpwI_GjtThqwCM

includes a request header

_ga=GA1.1.1963855; _ga_8F0NMFFQ6D=GS1.1.1655.1.1.1672903946.0.0.0; slack-app-oauth-state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsT3B0aW9ucyI6eyJzY29wZXMiOlsiYXBwX21lbnRpb25zOnJlYWQiLCJjaGF0OndyaXRlIiwiY2hhdDp3cml0ZS5wdWJsaWMiLCJjb21tYW5kcyIsImdyb3Vwczp3cml0ZSIsImltOndyaXRlIiwibXBpbTp3cml0ZSIsInVzZXJzOnJlYWQuZW1haWwiLCJ1c2VyczpyZWFkIl19LCJub3ciOiIyMDIzLTAxLTMxVDIxOjM3OjE3LjIwMVoiLCJyYW5kb20iOjY2OTMxMSwiaWF0IjoxNjc1MjAxMDM3fQ.jFYu4tWbqB7uREQba28W3u-zBrfxVTpwI_GjtThqwCM

The state value matches in all URL parameters and slack-app-oauth-state cookies but it still fails with the InvalidStateError. I'm puzzled.

@multimanic
Copy link
Author

@seratch can you confirm that my understanding of the code is correct?
Or does the network trace give you any clues what the problem might be?

@seratch
Copy link
Member

seratch commented Feb 7, 2023

@multimanic I am still unsure about the cause of your issue here, but one possibility is that your app on Firebase Hosting platform may be failing to access the request header data including cookies. Adding some print debug logs like console.log(request.headers) (note that this is pseudo code) and seeing the difference between the app on your local machine and on Firebase may help. If that's not the case, I don't have any meaningful suggestion on this.

@multimanic
Copy link
Author

Your instinct is good. I just found https://firebase.google.com/docs/hosting/manage-cache#using_cookies says

When using Firebase Hosting together with Cloud Functions or Cloud Run, cookies are generally stripped from incoming requests. This is necessary to allow for efficient CDN cache behavior. Only the specially-named __session cookie is permitted to pass through to the execution of your app.

When present, the __session cookie is automatically made a part of the cache key, meaning that it's impossible for two users with different cookies to receive the other's cached response. Only use the __session cookie if your app serves different content depending on user authorization.

I will test what happens if I use stateCookieName = '__session', and post an update for future reference.

@multimanic
Copy link
Author

SOLUTION - SOLUTION - SOLUTION - SOLUTION - SOLUTION

It is possible to use Firebase Cloud Functions with Firebase Hosting with distributed apps (using Bolt JS's /slack/install). If you get the error slack_oauth_invalid_state it's because Firebase Hosting together with Cloud Functions or Cloud Run, cookies are generally stripped from incoming requests - except a cookie called __session. The solution is to make use of the parameter stateCookieName the framework provides which allows you to rename the cookie from the default slack-app-oauth-state to __session.

Example:

const config = require('./config/config');

const functions = require('firebase-functions');
const firebase = require('firebase-admin');
const { App, ExpressReceiver } = require('@slack/bolt');
const { LogLevel } = require("@slack/logger");

const firebaseApp = firebase.initializeApp(
    functions.config().firebase
);
const db = firebaseApp.firestore();
firebase.firestore().settings({
    ignoreUndefinedProperties: true,
})

const expressReceiver = new ExpressReceiver({
    logLevel: LogLevel.DEBUG,
    signingSecret: config.slack.signingSecret,
    clientId: config.slack.clientId,
    clientSecret: config.slack.clientSecret,
    stateSecret: config.slack.stateSecret,
    scopes: config.slack.scopes,
    endpoints: '/slack/events',
    processBeforeResponse: true,
    installationStore: {
        storeInstallation: async (installation) => {
            if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
                return await db.collection(config.firebase.slackTeams).doc(installation.enterprise.id).set(installation);
            }
            if (installation.team !== undefined) {
                return await db.collection(config.firebase.slackTeams).doc(installation.team.id).set(installation);
            }
            throw new Error('Failed saving installation data to Firebase');
        },
        fetchInstallation: async (installQuery) => {
            if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
                return await getTeamRecord(installQuery.enterpriseId);
            }
            if (installQuery.teamId !== undefined) {
                return await getTeamRecord(installQuery.teamId);
            }
            throw new Error('Failed fetching installQuery');
        },
        deleteInstallation: async (installQuery) => {
            if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
                return await db.collection(config.firebase.slackTeams).doc(installQuery.enterpriseId).delete();
            }
            if (installQuery.teamId !== undefined) {
                return await db.collection(config.firebase.slackTeams).doc(installQuery.teamId).delete();
            }
            throw new Error('Failed to delete installQuery');
        },
    },
    installerOptions: {
        directInstall: false,
        stateVerification: true,
        stateCookieName: '__session',
    }
});

const app = new App({
    receiver: expressReceiver,
    processBeforeResponse: true,
    stateSecret: config.slack.stateSecret,
});

app.command('/say-hi', async ({ command, ack, say }) => {
    await ack();
    const { botToken } = await app.receiver.installer.authorize({teamId: command.team_id});
    app.client.chat.postMessage({
        token: botToken,
        channel: command.user_id,
        text: "Hi back!",
        as_user: true,
    });
});

exports.slack = functions.https.onRequest(expressReceiver.app);

@multimanic
Copy link
Author

How wise of you @seratch to make the cookie name configurable :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question M-T: User needs support to use the project
Projects
None yet
Development

No branches or pull requests

2 participants