Fancy GitHub Authentication with Omniauth

Configuring Omniauth for GitHub authentication is easy enough. But I needed to optionally add extra permissions to the authentication token. I eventually figured it out, but since I had to piece the steps together from various sources, I thought I’d document what I learned.

A basic Omniauth GitHub setup looks like this:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, github_id, github_secret
end

That just gives you a basic read-only authorization, however. If you want to go beyond that, for instance to update user details and manage repositories, you have to pass a “scope” option. The value of the option must be a comma-delimited string of scope names.

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, github_id, github_secret, scope: "user,repo"
end

Available scopes include user, public_repo, repo, and gist.

That’s all you need if you always want the same set of priviledges. But what if you want to authenticate some users with more priviledges than others?

The answer lies in the Omniauth “setup phase”. You can pass a :setup option to an Omniauth provider, with a proc which will be executed before the authentication is performed. Inside this proc you can query and alter the request environment. The Omniauth wiki has some basic documentation on how to do this.

I wanted to be able to pass an optional “role” parameter to the /auth/github action, with an app-specific meaning which would be used to determine which scopes to request. In order to encapsulate this logic I specced-out a GithubAuthOptions class:

describe GithubAuthOptions do
  subject { described_class.new(env) }
  context "given no special role" do
    let(:env) { Rack::MockRequest.env_for("/auth/github", params: {}) }

    its(:to_hash) { should be_empty }
    its(:to_hash) { should be_a(Hash) }
  end

  context "given a shopkeeper role" do
    let(:env) { 
      Rack::MockRequest.env_for("/auth/github", params: {'role' => 'shopkeeper'}) 
    }

    its(:to_hash) { should eq(:scope => 'repo') }
  end
end

I wrote the following implementation:

class GithubAuthOptions
  def initialize(env)
    @request = Rack::Request.new(env)
  end

  def to_hash
    if 'shopkeeper' == @request.params['role']
      {scope: 'repo'}
    else
      {}
    end
  end
end

Finally, I added GithubAuthOptions to my Omniauth initializer:

Rails.application.config.middleware.use OmniAuth::Builder do
  # ...

  provider :github, github_id, github_secret, setup: ->(env) {
    options = GithubAuthOptions.new(env)
    env['omniauth.strategy'].options.merge!(options.to_hash)
  }
end

The net result: if I redirect a user to plain /auth/github, they will be authenticated using the standard, read-only scope. But if I redirect them to /auth/github?role=shopkeeper, they will be authenticated with the “repo” scope as well, giving the app the ability to manager their public and private repositories.

3 comments

  1. Thanks for the post Avdi. Any advise on learning Rack and Middleware? I haven’t been able to wrap my head around Omniauth because of them. Or maybe the RR folks can do a discussion on that topic 🙂

    1. My suggestion is to just read the Rack docs and the source code for some middlewares. Then write your own middleware that does something silly like convert pages to pig latin.

Comments are closed.