Skip to content

Instantly share code, notes, and snippets.

@ginjo
Last active February 20, 2024 21:50
Show Gist options
  • Save ginjo/5fe43b4af8dd5b56725232548cda5d75 to your computer and use it in GitHub Desktop.
Save ginjo/5fe43b4af8dd5b56725232548cda5d75 to your computer and use it in GitHub Desktop.
OmniAuth::Slack OAuth2 Cycle Description and Integration Notes

OmniAuth::Slack OAuth2 Cycle Description and Integration Notes

This document was written for the ginjo-omniauth-slack ruby gem. It attempts to clarify the OAuth2 authorization cycle and how that cycle is implemented in your appliation with the ginjo-omniauth-slack gem.

OAuth2

The OAuth2 cycle is a three-way dance between the user's browser, the OAuth2 provider (Slack API), and the application server (your Slack App). It should work this way for any OAuth2 provider, including Slack.

  1. The user/browser makes a request to https://slack.com/oauth/authorize, passing the application's client-id, requested-scopes, and optionally state, team-id, and redirect-uri. Slack then runs the user through the authorization dialogs.

  2. Upon successful authorization, Slack redirects the browser to the application's callback url (or redirect_uri in Slack's terms) with a short-lived authorization code, for example:

    https://yourapp.com/auth/slack/callback?code=ABCDE87364

  3. Omniauth-slack intercepts this request and exchanges the code, via Slack API, for a valid access-token.

And that's it. Control is then given to your application's /auth/slack/callback action.

The next step would be the application storing the access-token, maybe making additional API requests, and then rendering a page to the browser or redirecting to another action. In a working app, a session would store a reference to the token, and the token would be stored in a database. Then for every request from that user, a valid access-token would be accessible and usable to make further API requests.

OAuth2 with OmniAuth::Slack

While the browser experience may appear simple, there's quite a lot happening behind the scenes. Omiauth-slack is running as part of the Rack stack, behind your main app, and it handles most of the above sequence before your application even receives the requests.

So lets run through the cycle again and take a closer look.

    1. The user clicks a browser button which makes a request to your application at https://yourapp.com/auth/slack. This URI is likely the source or href of your signin-with-slack or add-to-slack button.

      Your application doesn't need to know about this endpoint, and it doesn't need to define an action for it. Omniauth-slack middleware recognizes this URI as the authorization request.

    2. Omniauth-slack intercepts this request, considers your configuration, stores some data in a session variable, and then redirects the browser, with the necessary data embedded in the URI params, to Slack.

      OmniAuth calls this the request phase, and your application sees none of it.

    1. Having been redirected by omniauth-slack, the browser makes an authorization request to https://slack.com/oauth/authorize, passing the application's client-id, requested-scopes, and optionally state, team-id, and redirect-uri. This request contains everything Slack needs to authenticate the user and authorize access to Slack's API functions and data.

    2. Slack leads the user through any dialogs necessary to complete the authorization.

      Depending on the setup, the requested (and awarded) scopes and permissions, and Slack's internal logic, this cycle could appear as a series of dialogs or as a simple request/response. If identity scopes were requested (signin-with-slack flow), and a team-id was passed in the params, and the given scopes were previously authorized, Slack may grant authorization without requiring the user to click on any dialogs at all.

      Meanwhile, the application server and omniauth-slack are patiently waiting and have no idea what Slack, or the user, are doing at this point.

  1. Upon successful authorization, Slack redirects the browser to the application's callback url (or redirect_uri in Slack's terms) with a short-lived authorization code, for example:

    https://yourapp.com/auth/slack/callback?code=ABCDE87364.

    Your application needs to define an endpoint (a route, an action, a method, etc.) for /auth/slack/callback, but omniauth-slack does all of its work before your app even sees the request.

    1. Omniauth-slack intercepts this request, and exchanges the authorization code for a valid access-token by making an API request to https://slack.com/api/oauth.access.

      The oauth.access response contains an access-token (and possibly other data) which omniauth-slack stores in the Rack env for later use by your application.

      OmniAuth refers to this part of the process as the callback phase, and you don't see any of it (Rack middleware magic).

    2. Rack then passes this callback request to your app, and you are at the logical beginning of whatever action you defined for /auth/slack/callback.

      There is a lot of data available in the request env['omniauth.auth'] and env['omniauth.strategy']. There are also other env variables defined by omniauth and omniauth-slack. See the gem docs for more about those.

      At this point, you will likely want to grab the env['omniauth.auth'] hash and the env['omniauth.strategy'].access_token object. Use the access-token to make further API requests, or store the token and auth_hash for later retrieval.

      See the note about access tokens below.

Bare Bones Basic Examples

The above cycle could be implemented in a Sinatra or Rails setup as simple as the ones described below, but first...

Slack Settings

Before you try to implement omniauth-slack, create a Slack app on api.slack.com. Then setup the app's Redirect URL list in your Slack App's OAuth & Permissions section on api.slack.com. Set one or more Redirect URL entries that match the domain:port of this simple application. The app doesn't have to be accessible from the public internet, just from your local machine, for example: http://localhost:9292 or http://192.168.0.5:8000.

Sinatra Example

Create a Sinatra project directory, then add these files.

simple_app.rb

  require 'omniauth-slack'
  require 'sinatra'
  require 'yaml'

  enable :sessions

  # optional
  #set :port, '9292'
  #set :bind, '0.0.0.0'

  use OmniAuth::Builder do
    provider :slack, SLACK_OAUTH_KEY, SLACK_OAUTH_SECRET, scope:'identity:read:user'
  end

  get '/auth/slack/callback' do
    content_type 'text/yaml'
    { auth_hash:    env['omniauth.auth'],
      access_token: env['omniauth.strategy'].access_token
    }.to_yaml
  end

Gemfile

  source 'https://rubygems.org'
  gem 'ginjo-omniauth-slack'   #, git:'https://github.com/ginjo/omniauth-slack'
  gem 'sinatra'
  gem 'puma'

Put those in their respective files, fill in your Slack OAuth2 credentials, then launch.

bundle install
bundle exec ruby super_simple.rb

Then point your browser to

http://<host-and-port-recognized-in-slack-redirect-uri-list>/auth/slack

When a successful authorization cycle completes, your browser should end up with a yaml representation of the auth_hash and access_token objects. What happens next is entirely up to your application.

Rails Example

Create a rails project, then add or modify these files. Note that this is not necessarily the best way to do this in a production system. It's just a demonstration of the bare necessities to get omniauth-slack working in Rails.

config/initializers/middleware.rb

  require 'omniauth-slack'

  Rails.application.config.middleware.use OmniAuth::Builder do
    provider :slack, SLACK_OAUTH_KEY, SLACK_OAUTH_SECRET, scope:'identity:read:user'
  end

app/controllers/auth_controller.rb

  class AuthController < ApplicationController
    def callback
      render plain: { access_token: request.env['omniauth.strategy'].access_token.to_hash,
        auth_hash:  request.env['omniauth.auth']
      }.to_yaml
    end
  end

config/routes.rb

  get 'auth/slack/callback', to: 'auth#callback'

Gemfile

  gem 'ginjo-omniauth-slack'   #, git:'https://github.com/ginjo/omniauth-slack'

Don't forget to fill your Slack API credentials. Then start up Rails, and point your browser to

http://<host-and-port-recognized-in-slack-redirect-uri-list>/auth/slack

When a successful authorization cycle completes, your browser should end up with a yaml representation of the auth_hash and access_token objects. What happens next is entirely up to your application.

A Note about Access Tokens

While Slack's access token is a simple string, the OAuth2 gem packages it, along with any other data returned from the /api/oauth.access call, as an AccessToken instance. The OAuth2::AccessToken instance is a useful and often overlooked tool in the OAuth2 gem. With a valid AccessToken instance (generated from every successful OAuth2 cycle), you have the full spectrum of Slack API functionality at your fingertips.

The AccessToken contains everything you need to make Slack API requests: The actual token string, the expiration data, the team, user, scope, and an OAuth2::Client instance with the API key and secret.

The AccessToken generated by omniauth-slack also has additional features, such as has_scope?(list-of-scopes), which queries the token's awarded scopes. This is handy for Slack Workspace apps and their multi-dimensional scopes.

Storage

Use the AccessToken#to_hash method to prepare the token for serialization and storage in a database. This method strips off all unnecessary objects and leaves just the data.

Retrieval

When you want to reconstitute the access-token from a stored hash or string, use the OAuth2::AccessToken.from_hash method. Or use omniauth-slack's convenience method:

access_token = OmniAuth::Slack.build_access_token(key, secret, access_token_string_or_hash)

Usage

Once you have a valid AccessToken instance, you can do things like

  access_token.get('/api/apps.permissions.users.list')

  access_token.refresh

  access_token.post('/api/chat.postMessage', params: {channel: channel_id, text: message})

To extract data from the API response, call parsed on the response object.

  access_token.get('/api/channels.list').parsed['channels']

  # => [{'id' => 1, 'name' => ...}, {'id' => ...}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment