Skip to content

ThoughtWorksStudios/oauth2_provider

Repository files navigation

Introduction

This plugin implements v09 of the OAuth2 draft spec http://tools.ietf.org/html/draft-ietf-oauth-v2-09.
The latest version of the spec is available at http://tools.ietf.org/html/draft-ietf-oauth-v2

Currently only the web-server profile http://tools.ietf.org/html/draft-ietf-oauth-v2-09#section-1.4.1 is supported.

Hacking/Contributing

See HACKING.textile for information on how to hack on this code.

What is OAuth?

OAuth is an open-source specification for building a framework for allowing a third-party app (the “client”) to access protected resources from another application (the “provider,” or “resource owner”) at the request of a “user” of the client app. Oauth allows the user to enter his user credentials (ex. username and password) only to the provider app, which then grants the client app permission to view the protected resources on behalf of the user.

A very good overview of the basic OAuth workflow is here.

As some of this is not always easy to grok on first pass, we also put together some videos to give a step-by-step introduction to OAuth.

Common terms:

  • Provider/Resource Owner – the app that hosts the protected resource. A real world example is Twitter which uses OAuth as the protocol for all its clients.
  • Client – the app that requests to see the resource data on behalf of the user. Any Twitter client that shows tweets is an example of this.
  • User/end user – the entity who initiates the OAuth flow to allow the client to access protected data from the provider.
  • Client id/client secret – Often, provider apps will maintain a list of clients that are allowed to access their data. Client apps can be identified in a number of ways, including with an id and a secret.

What’s Included

  • The OAuth 2.0 (v9) plugin (RAILS_ROOT/vendor/plugins/oauth2_provider)
  • Units and functional tests (RAILS_ROOT/test)
  • A sample host Rails application (RAILS_ROOT)
    • Create sample app user at /users/new
    • Create sample app OAuth client at /oauth/clients
    • Access OAuth-allowed protected resource /protected_resource
    • List and revoke a user’s tokens at /oauth/user_tokens

Assumptions

Usage of this plugin assumes that you already have a RAILS app that has unique users who access user-specific data by logging in, or some other method of user authentication.

Supported Features

  • Web server flow as described at http://tools.ietf.org/html/draft-ietf-oauth-v2-09#section-1.4.1
  • Endpoint for clients to request authorization ‘code’
  • Endpoint to request an access token using the authorization ‘code’
  • Admin screens for:
    • End users to manage/revoke access tokens given out to 3rd party OAuth2 clients
    • Admins to manage OAuth2 clients
  • Admin controller actions for:
    • Delete a single token belonging to any user

Available Endpoint URLs

See config/routes.rb in the plugin for more details.

API only URLs

  • /oauth/authorize (Oauth2::Provider::OauthAuthorizeController) – used by the user-agent (browser) to request an authorization ‘code’
  • /oauth/token (Oauth2::Provider::OauthTokenController) – used by the OAuth2 client to request access token to access protected resources

Accessed from the browser

  • /oauth/user_tokens (Oauth2::Provider::OauthUserTokensController) – used by end users to view and revoke access to 3rd party OAuth2 clients.
  • /oauth/clients (Oauth2::Provider::OauthClientsController) – to manage oauth clients (should be available only to admins)
  • /oauth/user_tokens/revoke_by_admin (Oauth2::Provider::OauthUserTokensController) – used by users with admin privileges to revoke any one token (pass token_id param), or all tokens belonging to a single user (pass user_id param).

It is the responsibility of the host application to avoid routing conflicts. The simplest thing to do is avoid defining any paths starting with /oauth

It is also the responsibility to restrict access to admin-only features such as /oauth/user_tokens/revoke_by_admin

Installation

$ [sudo] gem install oauth2_provider

Add the following to your RAILS_ROOT/config/environment.rb:

Rails::Initializer.run do |config|
  config.gem 'oauth2_provider'
end

We recommend that you vendor all your gems:

$ rake gems:unpack $ rake gems:unpack:dependencies

Execute the configuration script from your RAILS_ROOT:

$ ./script/generate oauth2_provider

This will create:

  • config/initializers/oauth2_provider.rb
  • db/migrate/TIMESTAMP_create_oauth_authorizations.rb
  • db/migrate/TIMESTAMP_create_oauth_clients.rb
  • db/migrate/TIMESTAMP_create_oauth_tokens.rb

Configuration

Database

This plugin stores OAuth2 client, authorization codes, and access token into a DB table. It is therefore required that a host app create the necessary ActiveRecord migrations in their application.

  • In your RAILS_ROOT, run rake db:migrate, this will run all the migrations that were created as when you executed the oauth2_provider generator. You can’t do this step until you do the initializers file.

Rails initializer

Edit the file config/initializers/oauth2_provider.rb in which you

  • must call filter skipping methods on OauthTokenController, ensuring any authentication filters to not run for this controllers actions
  • must setup authorization for OauthClientsController, limiting access to only application administrators
  • must setup authorization for OauthUserTokensController actions, limiting access to all actions to logged in users (this part might come for free), and access to revoke_by_admin to administrators.
  • might call filter skipping or similar methods on the other provided controllers should it be necessary for them to run

A sample initializer:

module Oauth2
  module Provider
  
    # make sure no authentication for OauthTokenController
    OauthTokenController.skip_before_filter(:login_required)
    
    # use host app's custom authorization filter to protect OauthClientsController
    OauthClientsController.before_filter(:ensure_admin_user)
    
    # use host app's custom authorization filter to protect admin actions on OauthUserTokensController
    OauthUserTokensController.before_filter(:ensure_admin_user, :only => [:revoke_by_admin])
    
  end
end

ApplicationController

Make the following changes:

  • Include Oauth2::Provider::ApplicationControllerMethods module
  • Define a ‘current_user_id_for_oauth’ method that returns the id of the currently logged-in user, serving as a foreign-key to any tokens that are stored in the database.
  • alias_method_chain ‘user_id_for_oauth_access_token’ into your authentication filter. This method is supplied by Oauth2::Provider::ApplicationControllerMethods and converts an Oauth access token passed in a request’s header to a user_id known by the host application. This user_id corresponds to whatever your current_user_id method returns.

Before

class ApplicationController < ActionController::Base

  # the host application's authentication filter, you'll
  # see in the 'After' section below that you can user
  # alias_method_chain to merge OAuth 2.0 authentication
  # with your current authentication filter.
  before_filter :login_required

  # this checks whether the user is logged in for purposes
  # of an authentication filter. obviously, your host application
  # will have very different code than this.  this example is
  # pulled from the sample host application with which the plugin ships.
  def login_required
    current_user_id = session[:user_id]
    if current_user_id
      User.current = User.new(current_user_id)
    else
      raise "Lack of rights!"
    end
  end

end

After

class ApplicationController < ActionController::Base

  # the host application's authentication filter
  before_filter :login_required

  # include Oauth2::Provider::ApplicationControllerMethods
  include Oauth2::Provider::ApplicationControllerMethods
    
  # this checks whether the user is logged in for purposes
  # of an authentication filter. obviously, your host application
  # will have very different code than this.  this example is
  # pulled from the sample host application with which the plugin ships.
  def login_required
    current_user_id = session[:user_id]
    if current_user_id
      User.current = User.new(current_user_id)
    else
      raise "Lack of rights!"
    end
  end

  # required by the OAuth plugin to figure out the currently logged in user
  # must be a string representation of the user.
  # A 'username', 'email' or a db primary key are good candidates.
  protected def current_user_id_for_oauth
    super
  end

  # use alias_method_chain to wrap your existing authentication filter
  # with behavior that will use any valid passed Oauth access token header, 
  # falling back to your standard filter if the request is not an Oauth request.
  #
  # clearly, your host application will have different code than
  # this example, with different authentication models and different filter
  # method names.
  #
  # As for this example, OAuth2::Provider::ApplicationControllerMethods supplies 
  # a user_id_for_oauth_access_token to lookup the user_id for a given access token. 
  # The host app must then use that
  # user_id to setup the user session, do any authorization checks, etc.  in this
  # example (from the included sample host app) we simply store the user_id in 
  # the user's session.
  #
  # Note that user_id_for_oauth_access_token returns nil if an action
  # has not been enabled for Oauth (see below).
  def login_required_with_oauth
    if user_id = self.user_id_for_oauth_access_token
      session[:user_id] = user_id
    elsif looks_like_oauth_request?
      render :text => "Denied!", :status => :unauthorized
    else
      login_required_without_oauth
    end
  end
  alias_method_chain :login_required, :oauth

end

Oauth enable particular controller actions

By default, no action supports OAuth. That is, if you would like for OAuth to work for any particular action you must declare that in the controller. Below is a simple example where a concrete controller definition specifies where Oauth is allowed. If you look at the plugin source you’ll see that oauth_allowed can also be passed a block, returning true or false, for more sophisticated implementations.

class ProtectedResourceController < ApplicationController

  # Supported options are:
  #  :only => [:oauth_protected_action...]
  #  :except => [:oauth_unprotected_action...]
  # If no options are specified, defaults to oauth for all actions
  oauth_allowed :only => :index

  def index
    render :text => "current user is #{current_user.email}"
  end

  def no_oauth_here
    render :text => "this content not available via Oauth"
  end

end

Verify that everything is working correctly!

  • OAuth enable the actions for which you wish to allow OAuth as an authentication means
  • Setup a new OAuth client at /oauth/clients
  • Open /oauth/authorize?redirect_uri=REDIRECT_URI&client_id=CLIENT_ID&response_type=code in a browser. You should see a form asking you to authorize the client to access the host application on your behalf. Check the tickbox and submit.
  • Your browser is redirected to your redirect URI (hint: use example.com for this test) and you should now see your authorization code as the “code” parameter. Save this code, you will need it for the next step.
  • For this step you’ll need to use curl as you need to perform a POST and there is no form provided by the plugin. Use curl to POST to /oauth/token, passing the following key/value pairs as form data: code=AUTHORIZATION_CODE&grant_type=authorization-code&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&redirect_uri=REDIRECT_URI
  • The response to the above POST will be JSON containing your access token, and its expiration time in seconds.
{:access_token => ACCESS_TOKEN, :expires_in => ACCESS_TOKEN_EXPIRES_IN, :refresh_token => REFRESH_TOKEN}
  • You can now use the ACCESS_TOKEN to access the protected resource. This would require the ‘Authorization’ HTTP header (NOTE that the quotes are required around the ACCESS_TOKEN)
Authorization: Token token="ACCESS_TOKEN"
  • You should expect to see the contents of the protected resource now.

License

OAuth2 Provider Generator is MIT Licensed.

The MIT License

Copyright © 2010 ThoughtWorks, Inc. (http://thoughtworks.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.