This gem provides a toolkit for adding OAuth2 provider capabilities to a Ruby web app. It handles most of the protocol for you: it is designed to provide a sufficient level of abstraction that it can implement updates to the protocol without affecting your application code at all. All you have to deal with is authenticating your users and letting them grant access to client apps.
It is also designed to be usable within any web frontend, at least those of Rails and Sinatra. It assumes very little about the request objects in your environment, namely they:
-
respond to
#params
with aHash
of request parameters -
respond to
#get?
and#post?
-
respond to
#url
with the full URL string of the request -
respond to
#env
, returning the HTTP environmentHash
It stores the clients and authorizations using ActiveRecord.
The current implementation is based on draft-10.
-
Client: A third-party software system that integrates with the provider. Twitter and Facebook call this an “app”.
-
Client Owner: The entity which owns a client, i.e. the individual or company responsible for the client application.
-
Resource Owner: This will almost certainly be a User. It’s the entity which has the data that the client is asking permission to see.
-
Authorization: When a resource owner grants access to a client (i.e., a user grants access to a company’s app), an authorization is created. This can typically be revoked by the user at any time (which is the strength and flexibility of the OAuth architecture).
-
Access Token: An opaque string representing an authorization. A client is given an access token when a resource owner grants it access to resources. The access token must be included in all requests for protected resources.
A basic example is in example/application.rb
. To implement OAuth, you need to provide four things:
-
Some UI to register client applications
-
The OAuth request endpoint
-
A flow for logged-in users to grant access to clients
-
Resources protected by access tokens
If you are using Bundler, put this in your Gemfile:
gem 'oauth2-provider', :require => 'oauth2/provider', :git => 'git://github.com/songkick/oauth2-provider'
If you are not using Bundler, put the library in your load path and require it:
require 'oauth2/provider'
Declare your app’s name somewhere (for example in Rails, in application.rb
or an initializer):
OAuth2::Provider.realm = 'My OAuth app'
Your application should ensure that any endpoint that receives or returns OAuth data is only accessible over a secure transport such as the https:
protocol. OAuth2::Provider
can enforces this to make it easier to keep your users’ data secure.
You can set OAuth2::Provider.enforce_ssl = true
in the same place that you declared your app name above. This will result in the following behavior:
-
The
OAuth2::Provider.parse
method will produce error responses and will not process the incoming request unless the request was made using thehttps:
protocol. -
An access token constructed using
OAuth2::Provider.access_token
will returnfalse
for#valid?
unless the request was made using thehttps:
protocol. -
Any access token received over an insecure connection is immediately destroyed to prevent eavesdroppers getting access to the user’s resources. A client making an insecure request will have to send the user through the authorization process again to get a new token.
Add the OAuth2::Provider
tables to your app’s schema. This is done using OAuth2::Model::Schema.up
, which can be used inside an ActiveRecord
migration like so:
class CreateOauth2ProviderModels < ActiveRecord::Migration def up OAuth2::Model::Schema.up end def down OAuth2::Model::Schema.down end end
There are two mixins you need to put in your code, OAuth2::Model::ClientOwner
for whichever model will own the “apps”, and OAuth2::Model::ResourceOwner
for whichever model is the innocent, unassuming entity who will selectively share their data. It’s possible that this is the same model, such as User:
class User < ActiveRecord::Base include OAuth2::Model::ResourceOwner include OAuth2::Model::ClientOwner has_many :interesting_pieces_of_data end
Or they might go into two different models:
class User < ActiveRecord::Base include OAuth2::Model::ResourceOwner has_many :interesting_pieces_of_data end class Company < ActiveRecord::Base include OAuth2::Model::ClientOwner belongs_to :user end
To see the methods and associations that these two mixins add to your models, take a look at lib/oauth2/model/client_owner.rb and lib/oauth2/model/resource_owner.rb.
Clients are modeled by the OAuth2::Model::Client
class, which is an ActiveRecord model. You just need to implement a UI for creating them, for example in a Sinatra app:
get '/oauth/apps/new' do @client = OAuth2::Model::Client.new erb :new_client end post '/oauth/apps' do @client = OAuth2::Model::Client.new(params) @client.save ? erb(:show_client) : erb(:new_client) end
Client applications must have a name
and a redirect_uri
: provide fields for editing these but do not allow the other fields to be edited, since they are the client’s access credentials. When you’ve created the client, you should show its details to the user registering the client: its name
, redirect_uri
, client_id
and client_secret
(the last two are generated for you). client_secret
is not stored in plain text so you can only read it when you initially create the client object.
This is a path that your application exposes in order for clients to communicate with your application. It is also the page that the client will send users to so they can authenticate and grant access. Many requests to this endpoint will be protocol-level requests that do not involve the user, and OAuth2::Provider
gives you a generic way to handle all that.
You should use this to get the right response, status code and headers to send to the client. In the event that OAuth2::Provider
does not provide a response, you should render a page that lets the user begin to authenticate and grant access.
This endpoint must be accessible via GET and POST. In this example we will expose the OAuth service through the path /oauth/authorize
. We check if there is a logged-in resource owner and give this to OAuth::Provider
, since we may be able to immediately redirect if the user has already authorized the client:
[:get, :post].each do |method| __send__ method, '/oauth/authorize' do @owner = User.find_by_id(session[:user_id]) @oauth2 = OAuth2::Provider.parse(@owner, request) if @oauth2.redirect? redirect @oauth2.redirect_uri, @oauth2.response_status end headers @oauth2.response_headers status @oauth2.response_status @oauth2.response_body || erb(:login) end end
There is a set of parameters that you will need to hold on to for when your app needs to redirect back to the client. You could store them in the session, or pass them through forms as the user completes the flow. For example to embed them in the login form, do this:
<% @oauth2.params.each do |key, value| %> <input type="hidden" name="<%= key %>" value="<%= value %>"> <% end %>
You may also want to use scopes to provide granular access to your domain using scopes. The @oauth2
object exposes the scopes the client has asked for so you can display them to the user:
<p>The application <%= @oauth2.client.name %> wants the following permissions:</p> <ul> <% @oauth2.scopes.each do |scope| %> <li><%= PERMISSION_UI_STRINGS[scope] %></li> <% end %> </ul>
You can also use the method @oauth2.unauthorized_scopes
to get the list of scopes the user has not already granted to the client, in the case where the client already has some authorization. If no prior authorization exists between the user and the client, @oauth2.unauthorized_scopes
just returns all the scopes the client has asked for.
Once the user has authenticated you should show them a page to let them grant or deny access to the client application. This is straightforward; let’s say the user checks a box before posting a form to indicate their intent:
post '/oauth/allow' do @user = User.find_by_id(session[:user_id]) @auth = OAuth2::Provider::Authorization.new(@user, params) if params['allow'] == '1' @auth.grant_access! else @auth.deny_access! end redirect @auth.redirect_uri, @auth.response_status end
After granting or denying access, we just redirect back to the client using a URI that OAuth2::Provider
will provide for you.
If you like, OAuth lets you use a user’s login credentials to authenticate with a provider. In this case the client application must request these credentials directly from the user and then post them to the exchange endpoint. On the provider side you can handle this using the handle_passwords
and grant_access!
API methods, for example:
OAuth2::Provider.handle_passwords do |client, username, password| user = User.find_by_username(username) if user.authenticate?(password) user.grant_access!(client) else nil end end
The block must return user.grant_access!(client)
if you want to allow access, otherwise it should return nil
.
Assertions provide a way to access your OAuth services using user credentials from another service. When using assertions, the user will not authenticate on your web site; the OAuth client will authenticate the user using some other framework and obtain a token, then exchange this token for an access token on your domain.
For example, a client application may let a user authenticate using Facebook, so the application obtains a Facebook access token from the user. The client would then pass this token to your OAuth endpoint and exchange it for an access token from your site. You will typically create an account in your database to represent this, then have that new account grant access to the client.
To use assertions, you must tell OAuth2::Provider
how to handle assertions based on their type. An assertion type must be a valid URI. For the Facebook example we’d do the following. The block yields the Client
object making the exchange request, and the value of the assertion, which in this example will be a Facebook access token.
OAuth2::Provider.handle_assertions 'https://graph.facebook.com/me' do |client, assertion| facebook = URI.parse('https://graph.facebook.com/me?access_token=' assertion) response = Net::HTTP.get_response(facebook) user_data = JSON.parse(response.body) account = User.from_facebook_data(user_data) account.grant_access!(client) end
This code should run when your app boots, not during a request handler - think of it as configuration for OAuth2::Provider
. The framework will invoke it when a client attempts to use assertions with your OAuth endpoint.
The final call in your handler should be to grant_access!
; this returns an Authorization
object that the framework then uses to complete the response to the client. If you want to deny the request for whatever reason, the block must return nil
. If a client tries to use an assertion type you have no handler for, the client will get an error response.
To protect the user’s resources you need to check for access tokens. This is simple, for example a call to get a user’s notes:
get '/user/:username/notes' do user = User.find_by_username(params[:username]) token = OAuth2::Provider.access_token(user, ['read_notes'], request) headers token.response_headers status token.response_status if token.valid? JSON.unparse('notes' => user.notes) else JSON.unparse('error' => 'No notes for you!') end end
OAuth2::Provider.access_token()
takes a ResourceOwner
, a list of scopes required to access the resource, and a request object. If the token was not granted for the required scopes, has expired or is simply invalid, headers and a status code are set to indicate this to the client. token.valid?
is the call you should use to determine whether to serve the request or not.
It is also common to provide a dynamic resource for getting some basic data about a user by supplying their access token. This can be done by passing nil
as the resource owner:
get '/me' do token = OAuth2::Provider.access_token(nil, [], request) if token.valid? JSON.unparse('username' => token.owner.username) else JSON.unparse('error' => 'Keep out!') end end
token.owner
returns the ResourceOwner
that issued the token. A token represents the fact that a single owner gave a single client a set of permissions.
Copyright © 2010-2011 Songkick.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.