skip to main content

Declaring multiple sets of scopes for the same provider with Devise and OmniAuth in Rails

How to configure Devise and OmniAuth to offer two scopes of authentication to the same service

Nicolas Goutay

January 6, 2021

Photo by Stephen Phillips - Hostreviews.co.uk on Unsplash

If you’re familiar with the Rails ecosystem, the names Devise and OmniAuth might ring a bell: the former is a gem that handles (nearly) everything related to authentication; coupled with the latter, it makes implementing popular Social Login providers (e.g. Login with Facebook, or Twitter, or GitHub…) a breeze.

They can also be used to abstract away the whole OAuth dance that developers need to wrangle with every time they want to connect to a third-party API. We use it extensively at Orbit to authenticate with the GitHub, Twitter, Discourse, and Slack APIs, allowing us to build powerful integrations on top of those.

Take Slack, for example. Our Slack App connects Orbit to our users’ Slack workspaces to send notifications and provide a handy /orbit command.

The command /orbit add github:phacks added a new member to our Orbit community

Here’s what the OmniAuth provider for that looks like:

config.omniauth :slack,
                ENV['SLACK_CLIENT_ID'],
                ENV['SLACK_CLIENT_SECRET'],
                scope: 'commands,chat:write,chat:write.public,channels:read'

In some cases, however, you might need two different sets of scopes for two distinct integrations with the same service.

As we’re building our Slack Integration, which will allow users to gather and analyze the activity in their community Slack, we realized that we needed users to authorize to Slack twice: once on their team Slack, for the Slack App, and once on their community Slack, for the Slack integration. Moreover, the required scopes would differ wildly: as the Slack App needs to post messages and respond to slash commands, the Slack integration would only need to listen to events (e.g. somebody joined, or posted a message).

Adding both sets of scopes to our single OmniAuth provider would have worked, but it is considered (rightly) a security risk to ask for too broad a scope: in our case, the Slack App has no business listening to new messages and the Slack Integration shouldn’t be able to post messages in a channel.

So we needed to create two sets of scopes (one for the App, one for the Integration) for the same provider (Slack).

The first step was to rename our existing provider (the one above) to :slack_app. By doing this however, we lose the implicit binding of that provider to the Slack strategy—which we can hopefully add back with the strategy_class option:

config.omniauth :slack_app,
                ENV['SLACK_APP_CLIENT_ID'],
                ENV['SLACK_APP_CLIENT_SECRET'],
                scope: 'commands,chat:write,chat:write.public,channels:read',
                strategy_class: OmniAuth::Strategies::Slack

This gets us close, but not yet there: this config will set the provider attribute of the OAuth return payload to slack, not slack_app—meaning the callback route cannot know whether this particular user authorized the Slack App or the Slack Integration.

We can get around this by adding the name: slack_app option, which will do two things:

  • Set the provider attribute of the OAuth return payload to the right value, and
  • Change the OAuth callback route to /users/auth/slack_app/callback instead of /users/auth/slack/callback. (If you’re curious, here’s the bit of code in OmniAuth that’s responsible for inferring the callback URL.)

After changing the app/controllers/users/omniauth_callbacks_controller.rb to reflect the change in the URL (slack becomes slack_app), everything is running smoothly again.

We can now add our second provider for the Slack Integration, with its distinct name and scope.

config.omniauth :slack_app,
                ENV['SLACK_APP_CLIENT_ID'],
                ENV['SLACK_APP_CLIENT_SECRET'],
                name: 'slack_app',
                scope: 'commands,chat:write,chat:write.public,channels:read',
                strategy_class: OmniAuth::Strategies::Slack

config.omniauth :slack_integration,
                ENV['SLACK_INTEGRATION_CLIENT_ID'],
                ENV['SLACK_INTEGRATION_CLIENT_SECRET'],
                name: 'slack_integration',
                scope:
                  'channels:history,channels:read,reactions:read,users:read.email,users.profile:read',
                strategy_class: OmniAuth::Strategies::Slack

Tada! Our users can now authorize only minimal scopes for the App, the Integration, our both—a win for security!

OAuth can often feel confusing, and I want to take this opportunity to thank the Devise and OmniAuth maintainers and contributors who are doing a remarkable job to make it easier for the rest of us.

Hope this article can help folks facing the same issues we did!

You might also like:

Why Orbit is Better Than Funnel for Developer Relations
DevRel teams need tools and models created specifically for our discipline, and not just those adopted from other fields.
Slack vs Discord vs Discourse: The best tool for your community
An in-depth comparison of 3 top community platforms across dozens of factors.
How we use Orbit to build Orbit
A guide to how we use our product to build our community.

Join our mailing list  📩

No spam, just insights, product and content updates, event invites, and the occasional space pun.

Orbit is a registered trademark of Orbit Labs, Inc.

Talk to Team Orbit.


We can't wait to learn more about your company and community. Just complete this form and we'll be in touch soon.

nevermind