Coder Social home page Coder Social logo

assaf / rack-oauth2-server Goto Github PK

View Code? Open in Web Editor NEW
232.0 15.0 76.0 2.66 MB

LOOKING FOR MAINTAINER — OAuth 2.0 Authorization Server as a Rack module

Home Page: http://documentup.com/assaf/rack-oauth2-server/

License: MIT License

Ruby 71.04% CSS 2.80% CoffeeScript 4.90% JavaScript 21.26%

rack-oauth2-server's Introduction

Rack::OAuth2::Server

OAuth 2.0 Authorization Server as a Rack module. Because you don't allow strangers into your app, and OAuth 2.0 is the new awesome.

rack-oauth2-server currently implements version 10 of the OAuth 2.0 spec http://tools.ietf.org/html/draft-ietf-oauth-v2-10.

Build Status

For more background, check out the presentation slides.

Adding OAuth 2.0 To Your Application

Step 1: Setup Your Database

The authorization server needs to keep track of clients, authorization requests, access grants and access tokens. That could only mean one thing: a database.

The current release uses MongoDB. You're going to need a running server and open connection in the form of a Mongo::DB object. Because MongoDB is schema-less, there's no need to run migrations.

If MongoDB is not your flavor, you can easily change the models to support a different database engine. All the persistence logic is located in lib/rack/oauth2/models and kept simple by design. And if you did the work to support a different database engine, send us a pull request.

Step 2: Use The Server

For Rails 2.3/3.0, Rack::OAuth2::Server automatically adds itself as middleware when required, but you do need to configure it from within config/environment.rb (or one of the specific environment files). For example:

Rails::Initializer.run do |config|
  . . .
  config.after_initialize do
    config.oauth.database = Mongo::Connection.new["my_db"]
    config.oauth.authenticator = lambda do |username, password|
      user = User.find(username)
      user.id if user && user.authenticated?(password)
    end
  end
end

For Sinatra and Padrino, first require rack/oauth2/sinatra and register Rack::OAuth2::Sinatra into your application. For example:

require "rack/oauth2/sinatra"

class MyApp < Sinatra::Base
  register Rack::OAuth2::Sinatra

  oauth.database = Mongo::Connection.new["my_db"]
  oauth.authenticator = lambda do |username, password|
    user = User.find(username)
    user if user && user.authenticated?(password)
  end

  . . .
end

With any other Rack server, you can use Rack::OAuth2::Server and pass your own Rack::OAuth2::Server::Options object.

The configuration options are:

  • :access_token_path- Path for requesting access token. By convention defaults to /oauth/access_token.
  • :authenticator - For username/password authorization. A block that receives the credentials and returns identity string (e.g. user ID) or nil.
  • :authorization_types - Array of supported authorization types. Defaults to `["code", "token"]``, and you can change it to just one of these names.
  • :authorize_path - Path for requesting end-user authorization. By convention defaults to /oauth/authorize.
  • :database - Mongo::DB instance (this is a global setting).
  • :expires_in - Number of seconds an auth token will live. If nil or zero, access token never expires.
  • :host - Only check requests sent to this host.
  • :path - Only check requests for resources under this path.
  • :param_authentication - If true, supports authentication using query/form parameters.
  • :realm - Authorization realm that will show up in 401 responses. Defaults to use the request host name.
  • :logger - The logger to use. Under Rails, defaults to use the Rails logger. Will use Rack::Logger if available.
  • :collection_prefix - Prefix to use for MongoDB collections created by rack-oauth2-server. Defaults to oauth2.

Authenticator

If you only intend to use the UI authorization flow, you don't need to worry about the authenticator. If you want to allow client applications to create access tokens by passing the end-user's username/password, then you need an authenticator. This feature is necessary for some client applications, and quite handy during development/testing.

The authenticator is a block that receives either two or four parameters. The first two are username and password. The other two are the client identifier and scope. It authenticated, it returns an identity, otherwise it can return nil or false. For example:

oauth.authenticator = lambda do |username, password|
  user = User.find_by_username(username)
  user.id if user && user.authenticated?(password)
end

Assertion Handler

The gem will automatically handle JWT assertions. If you want to be able to configure your own function to handle custom assertion types, you can follow this example for "facebook.com" that will allow the following use case:

  1. Mobile device authenticates with Facebook, receives access_token.
  2. Mobile device sends access_token to the server as an assertion
  3. If the server recognizes that Facebook access_token as belonging to an existing user, return our oauth access_token as a normal access_token request; if it doesn't return unauthorized 401.

In application.rb or other initializer with scope to config.oauth:

config.oauth.assertion_handler['facebook.com'] = lambda do |client, assertion, scope|
  Rails.logger.debug("Assertion: #{assertion}")
  graph = Koala::Facebook::GraphAPI.new(assertion)
  begin
    user_data = graph.get_object('me')
    Rails.logger.debug("FB User Data: #{user_data}")
    user = User.find_by_facebook_auth({ :uid => user_data['id']})
  rescue Exception => e
    # fall through
    Rails.logger.debug("Could not find/load Facebook user: #{assertion} / #{e}")
  end
  if user
    Rails.logger.debug('Valid Facebook Assertion')
    user.id.to_s # Requires a string or integer
  else
    Rails.logger.debug('Invalid Facebook Assertion')
    nil
  end
end

If you want this to be called, then your client needs to send its assertion like this (JSON format here):

{
  client_id: <apiClientId>,
  client_secret: <apiClientSecret>,
  grant_type: 'assertion',
  assertion_type: 'facebook.com',
  assertion: <fb access token>
}

Step 3: Let Users Authorize

Authorization requests go to /oauth/authorize. Rack::OAuth2::Server intercepts these requests and validates the client ID, redirect URI, authorization type and scope. If the request fails validation, the user is redirected back to the client application with a suitable error code.

If the request passes validation, Rack::OAuth2::Server sets the request header oauth.authorization to the authorization handle, and passes control to your application. Your application will ask the user to grant or deny the authorization request.

Once granted, your application signals the grant by setting the response header oauth.authorization to the authorization handle it got before, and setting the response header oauth.identity to the authorized identity. This is typicaly the user ID or account ID, but can be anything you want, as long as it's a string. Rack::OAuth2::Server intercepts this response and redirects the user back to the client application with an authorization code or access token.

To signal that the user denied the authorization requests your application sets the response header oauth.authorization as before, and returns the status code 403 (Forbidden). Rack::OAuth2::Server will then redirect the user back to the client application with a suitable error code.

In Rails, the entire flow would look something like this:

class OauthController < ApplicationController
  def authorize
    if current_user
      render :action=>"authorize"
    else
      redirect_to :action=>"login", :authorization=>oauth.authorization
    end
  end

  def grant
    head oauth.grant!(current_user.id)
  end

  def deny
    head oauth.deny!
  end
end

Rails actions must render something. The oauth method returns a helper object (Rack::OAuth2::Server::Helper) that cannot render anything, but can set the right response headers and return a status code, which we then pass on to the head method.

In Sinatra/Padrino, it would look something like this:

get "/oauth/authorize" do
  if current_user
    render "oauth/authorize"
  else
    redirect "/oauth/login?authorization=#{oauth.authorization}"
  end
end

post "/oauth/grant" do
  oauth.grant! "Superman"
end

post "/oauth/deny" do
  oauth.deny!
end

The view would look something like this:

<h2>The application <% link_to h(oauth.client.display_name), oauth.client.link %>
  is requesting to <%= oauth.scope.to_sentence %> your account.</h2>
<form action="/oauth/grant">
  <button>Grant</button>
  <input type="hidden" name="authorization" value="<%= oauth.authorization %>">
</form>
<form action="/oauth/deny">
  <button>Deny</button>
  <input type="hidden" name="authorization" value="<%= oauth.authorization %>">
</form>

Step 4: Protect Your Path

Rack::OAuth2::Server intercepts all incoming requests and looks for an Authorization header that uses OAuth authentication scheme, like so:

Authorization: OAuth e57807eb99f8c29f60a27a75a80fec6e

It can also support the oauth_token query parameter or form field, if you set param_authentication to true. This option is off by default to prevent conflict with OAuth 1.0 callback.

If Rack::OAuth2::Server finds a valid access token in the request, it sets the request header oauth.identity to the value you supplied during authorization (step 3). You can use oauth.identity to resolve the access token back to user, account or whatever you put there.

If the access token is invalid or revoked, it returns 401 (Unauthorized) to the client. However, if there's no access token, the request goes through. You might want to protect some URLs but not others, or allow authenticated and unauthenticated access, the former returning more data or having higher rate limit, etc.

It is up to you to reject requests that must be authenticated but are not. You can always just return status code 401, but it's better to include a proper WWW-Authenticate header, which you can do by setting the response header oauth.no_access to true, or using oauth_required to setup a filter.

You may also want to reject requests that don't have the proper scope. You can return status code 403, but again it's better to include a proper WWW-Authenticate header with the required scope. You can do that by setting the response header oauth.no_scope to the scope name, or using oauth_required with the scope option.

In Rails, it would look something like this:

class MyController < ApplicationController

  before_filter :set_current_user
  oauth_required :only=>:private
  oauth_required :only=>:calc, :scope=>"math"

  # Authenticated/un-authenticated get different responses.
  def public
    if oauth.authenticated?
      render :action=>"more-details"
    else
      render :action=>"less-details"
    end
  end

  # Must authenticate to retrieve this.
  def private
    render
  end

  # Must authenticate with scope math to do this.
  def calc
    render :text=>"2+2=4"
  end

protected

  def set_current_user
    @current_user = User.find(oauth.identity) if oauth.authenticated?
  end

end

In Sinatra/Padrino, it would look something like this:

before do
  @current_user = User.find(oauth.identity) if oauth.authenticated?
end

oauth_required "/private"
oauth_required "/calc", :scope=>"math"

# Authenticated/un-authenticated get different responses.
get "/public" do
  if oauth.authenticated?
    render "more-details"
  else
    render "less-details"
  end
end

# Must authenticate to retrieve this.
get "/private" do
  render "secrets"
end

# Must authenticate with scope math to do this.
get "/calc" do
  render "2 + 2 = 4"
end

Step 5: Register Some Clients

Before a client application can request access, there must be a client record in the database. Registration provides the client application with a client ID and secret. The client uses these to authenticate itself.

The client provides its display name, site URL and image URL. These should be shown to the end-user to let them know which client application they're granting access to.

Clients can also register a redirect URL. This is optional but highly recommended for better security, preventing other applications from hijacking the client's ID/secret.

You can register clients using the command line tool oauth2-server:

$ oauth2-server register --db my_db

Or you can register clients using the Web-based OAuth console, see below.

Programatically, registering a new client is as simple as:

$ ./script/console
Loading development environment (Rails 2.3.8)
> client = Rack::OAuth2::Server.register(:display_name=>"UberClient",
   :link=>"http://example.com/",
   :image_url=>"http://farm5.static.flickr.com/4122/4890273282_58f7c345f4.jpg",
   :scope=>%{read write},
   :redirect_uri=>"http://example.com/oauth/callback")
> puts "Your client identifier: #{client.id}"
> puts "Your client secret: #{client.secret}"

You may want your application to register its own client application, always with the same client ID and secret, which are also stored in a configuration file. For example, your db/seed.rb may contain:

oauth2 = YAML.load_file(Rails.root + "config/oauth2.yml")
Rack::OAuth2::Server.register(id: oauth2["client_id"], secret: oauth2["client_secret"],
  display_name: "UberClient", link: "http://example.com",
  redirect_uri: "http://example.com/oauth/callback", scope: oauth2["scope"].split)

When you call register with id and secret parameters it either registers a new client with these specific ID and sceret, or if a client already exists, updates its other properties.

Step 6: Pimp Your API

I'll let you figure that one for yourself.

Two-legged OAuth flow

Rack::OAuth2::Server also supports the so-called "two-legged" OAuth flow, which does not require the end user authorization process. This is typically used in server to server scenarios where no user is involved. To utilize the two-legged flow, send the grant_type of "none" along with the client_id and client_secret to the access token path, and a new access token will be generated (assuming the client_id and client_secret check out).

Assertions

Rack::OAuth2::Server supports the use of assertions (in the form of the assertion grant_type) to obtain access tokens. Currently JSON Web Tokens (JWT) are the only supported assertion_type.

http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3

http://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03

In order to verify the signatures of assertions, you will need to create assertion Issuers. You can register assertion Issuers using the command line tool oauth2-server:

$ oauth2-server register_issuer --db my_db

Programatically, registering a new Issuer is as simple as:

$ ./script/console
  Loading development environment (Rails 2.3.8)
> issuer = Rack::OAuth2::Server.register_issuer(:identifier => "http://www.someidp.com",
   :hmac_secret => "foo",
   :public_key => "-----BEGIN RSA PUBLIC KEY-----\n....\n...=\n-----END RSA PUBLIC KEY-----\n")

When you call register_issuer it either registers a new issuer with these specific values, or if an issuer already exists with the given identifier it will update it's properties.

Depending on the algorithm used for signing the assertion (HMAC SHA or RSA), you pass either :hmac_secret or :public_key to Rack::OAuth2::Server.register_issuer (or both if you will use both with a single issuer). The value of :public_key can be either a PEM or DER encoded public key (as supported by OpenSSL::PKey::RSA.new).

Rack::OAuth2::Server validates that the issuer (iss), principal (prn), audience (aud) and expiration (exp) claims are present. It also validates that the expiration claim has not passed (with a 10 minute padding added to account for server clock skew).

OAuth Web Admin

We haz it, and it's pretty rad!

Web admin

To get the Web admin running, you'll need to do the following. First, you'll need to register a new client application that can access the OAuth Web admin, with the scope oauth-scope and redirect_uri that points to where you plan the Web admin to live. This URL must end with /admin, for example, http://example.com/oauth/admin.

The easiest way to do this is to run the oauth2-sever command line tool:

$ oauth2-server setup --db my_db

Next, in your application, make sure to ONLY AUTHORIZE ADMINISTRATORS to access the Web admin, by granting them access to the oauth-admin scope. For example:

def grant
  # Only admins allowed to authorize the scope oauth-admin
  if oauth.scope.include?("oauth-admin") && !current_user.admin?
    head oauth.deny!
  else
    head oauth.grant!(current_user.id)
  end
end

Make sure you do that, or you'll allow anyone access to the OAuth Web admin.

After this, remember to include the server admin module in your initializer (environemnt.rb or application.rb), because this is an optional feature:

require "rack/oauth2/server/admin"

Next, mount the OAuth Web admin as part of your application, and feed it the client ID/secret. For example, for Rails 2.3.x add this to config/environment.rb:

Rails::Initializer.run do |config|
  . . .
  config.after_initialize do
    config.middleware.use Rack::OAuth2::Server::Admin.mount
    Rack::OAuth2::Server::Admin.set :client_id, "4dca20453e4859cb000007"
    Rack::OAuth2::Server::Admin.set :client_secret, "981fa734e110496fcf667cbf52fbaf03"
    Rack::OAuth2::Server::Admin.set :scope, %w{read write}
  end
end

For Rails 3.0.x, add this to you config/application.rb:

  module MyApp
    class Application < Rails::Application
      config.after_initialize do
        Rack::OAuth2::Server::Admin.set :client_id, "4dca20453e4859cb000007"
        Rack::OAuth2::Server::Admin.set :client_secret, "981fa734e110496fcf667cbf52fbaf03"
        Rack::OAuth2::Server::Admin.set :scope, %w{read write}
      end
    end
  end

And add the follownig to config/routes.rb:

mount Rack::OAuth2::Server::Admin=>"/oauth/admin"

For Sinatra, Padrino and other Rack-based applications, you'll want to mount like so (e.g. in config.ru):

Rack::Builder.new do
  map("/oauth/admin") { run Rack::OAuth2::Server::Admin }
  map("/") { run MyApp }
end
Rack::OAuth2::Server::Admin.set :client_id, "4dca20453e4859cb000007"
Rack::OAuth2::Server::Admin.set :client_secret, "981fa734e110496fcf667cbf52fbaf03"
Rack::OAuth2::Server::Admin.set :scope, %w{read write}

Next, open your browser to http://example.com/oauth/admin, or wherever you mounted the Web admin.

Web Admin Options

You can set the following options:

  • client_id - Client application identified, require to authenticate.
  • client_secret - Client application secret, required to authenticate.
  • authorize - Endpoint for requesing authorization, defaults to /oauth/admin.
  • template_url - Will map an access token identity into a URL in your application, using the substitution value {id}, e.g. http://example.com/users/#{id})
  • force_ssl - Forces all requests to use HTTPS (true by default except in development mode).
  • scope - Common scope shown and added by default to new clients (array of names, e.g. `["read", "write"]``).

Web Admin API

The OAuth Web admin is a single-page client application that operates by accessing the OAuth API. The API is mounted at /oauth/admin/api (basically /api relative to the UI), you can access it yourself if you have an access token with the scope oauth-admin.

The API is undocumented, but between the very simple Sinatra code that provides he API, and just as simple Sammy.js code that consumes it, it should be easy to piece together.

OAuth 2.0 With Curl

The premise of OAuth 2.0 is that you can use it straight from the command line. Let's start by creating an access token. Aside from the UI authorization flow, OAuth 2.0 allows you to authenticate with username/password. You'll need to register an authenticator, see step 2 above for details.

Now make a request using the client credentials and your account username/password, e.g.:

$ curl -i http://localhost:3000/oauth/access_token \
  -F grant_type=password \
  -F client_id=4dca20453e4859cb000007 \
  -F client_secret=981fa734e110496fcf667cbf52fbaf03 \
  -F "scope=read write" \
  -F [email protected] \
  -F password=not.telling

This will spit out a JSON document, something like this:

{ "scope":"import discover contacts lists",
  "access_token":"e57807eb99f8c29f60a27a75a80fec6e" }

Grab the access_token value and use it. The access token is good until you delete it from the database. Making a request using the access token:

$ curl -i http://localhost:3000/api/read -H "Authorization: OAuth e57807eb99f8c29f60a27a75a80fec6e"

Although not recommended, you can also pass the token as a query parameter, or when making POST request, as a form field:

$ curl -i http://localhost:3000/api/read?oauth_token=e57807eb99f8c29f60a27a75a80fec6e
$ curl -i http://localhost:3000/api/update -F name=Superman -F oauth_token=e57807eb99f8c29f60a27a75a80fec6e

You'll need to set the option param_authentication to true. Watch out, since this query parameter could conflict with OAuth 1.0 authorization responses that also use oauth_token for a different purpose.

Here's a neat trick. You can create a .curlrc file and load it using the -K option:

$ cat .curlrc
header = "Authorization: OAuth e57807eb99f8c29f60a27a75a80fec6e"
$ curl -i http://localhost:3000/api/read -K .curlrc

If you create .curlrc in your home directory, curl will automatically load it. Convenient, but dangerous, you might end up sending the access token to any server you curl. Useful for development, testing, just don't use it with any production access tokens.

Example using the assertion grant_type:

$ curl -i  http://localhost:3000/oauth/access_token \
    -F client_id=4dca20453e4859cb000007 \
    -F client_secret=981fa734e110496fcf667cbf52fbaf03 \
    -F grant_type=assertion \
    -F assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer \
    -F assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vd3d3LnNvbWVjb21wYW55LmNvbSIsImF1ZCI6Imh0dHA6Ly93d3cubXljb21wYW55LmNvbSIsInBybiI6IjEyMzQ1Njc4OTAifQ.bDrcogybtJ9n5d2a971Q72ye7GN64u7WXmr2OLxSeyc

Methods You'll Want To Use From Your App

You can use the Server module to create, fetch and otherwise work with access tokens and grants. Available methods include:

  • access_grant - Creates and returns a new access grant. You can use that for one-time token, e.g. users who forgot their password and need to login using an email message.
  • token_for -- Returns access token for particular identity. You can use that to give access tokens to clients other than through the OAuth 2.0 protocol, e.g. if you let users authenticate using Facebook Connect or Twitter OAuth.
  • get_access_token -- Resolves access token (string) into access token (AccessToken object).
  • list_access_tokens -- Returns all access tokens for a given identity, which you'll need if you offer a UI for uses to review and revoke access tokens they previously granted.
  • get_client -- Resolves client identifier into a Client` object.
  • register -- Registers a new client application. Can also be used to change existing registration (if you know the client's ID and secret). Idempotent, so perfect for running during setup and migration.
  • get_auth_request -- Resolves authorization request handle into an AuthRequest object. Could be useful during the authorization flow.
  • register_issuer -- Registers a new assertion issuer. Can also be used to change existing issuers. Idempotent, so perfect for running during setup and migration.
  • get_issuer -- Resolves an issuer identifier into an Issuer object.

Mandatory ASCII Diagram

This is briefly what the authorization flow looks like, how the workload is split between Rack::OAuth2::Server and your application, and the protocol the two use to control the authorization flow:

                           Rack::OAuth2::Server
              -----------------------    -----------------------
Client app    | /oauth/authorize    |    | Set request.env     |
redirect   -> |                     | -> |                     | ->
              | authenticate client |    | oauth.authorization |
              -----------------------    -----------------------

                                  Your code
   --------------------     ----------------------    -----------------------
   | Authenticate user |    | Ask user to grant/ |    | Set response        |
-> |                   | -> | deny client access | -> |                     | ->
   |                   |    | to their account   |    | oauth.authorization |
   |                   |    |                    |    | oauth.identity      |
   --------------------     ----------------------    -----------------------

    Rack::OAuth2::Server
   -----------------------
   | Create access grant |
-> | or access token for | -> Redirect back
   | oauth.identity      |    to client app
   -----------------------

Understanding the Models

Client

The Rack::OAuth2::Server::Client model represents the credentials of a client application. There are two pairs: the client identifier and secret, which the client uses to identify itself to the authorization server, and the display name and URL, which the client uses to identify itself to the end user.

The client application is not tied to a single Client record. Specifically, if the client credentials are compromised, you'll want to revoke it and create a new Client with new pair of identifier/secret. You can leave the revoked instance around.

Calling revoke! on the client revokes access using these credential pair, and also revokes any outstanding authorization requests, access grants and access tokens created using these credentials.

You may also want to register a redirect URI. If registered, the client is only able to request authorization that redirect back to that redirect URI.

Authorization Request

The authorization process may involve multiple requests, and the application must maintain the authorization request details from beginning to end.

To keep the application simple, all the necessary information for a single authorization request is stored in the Rack::OAuth2::Server::AuthRequest model. The application only needs to keep track of the authorization request identifier.

Granting an authorization request (by calling grant!) creates an access grant or access token, depending on the requested response type, and associates it with the identity.

Access Grant

An access grant (Rack::OAuth2::Server::AccessGrant) is a nonce use to generate access token. This model keeps track of the nonce (the "authorization code") and all the data it needs to create an access token.

Access Token

An access token allows the client to access the resource with the given scope on behalf of a given identity. It keeps track of the account identifier (supplied by the application), client identifier and scope (both supplied by the client).

An Rack::OAuth2::Server::AccessToken is created by copying values from an AuthRequest or AccessGrant, and remains in effect until revoked. OAuth 2.0 access tokens can also expire, Rack::OAuth2::Server::AccessToken has expires_at field.

Issuer

An issuer is a identity provider which issues assertions that may be used to obtain an access token.

Tests

Start tests for Rails

  bundle install
  bundle exec rake test

Credits

Rack::OAuth2::Server was written to provide authorization/authentication for the Flowtown API. Thanks to Flowtown for making it happen and allowing it to be open sourced.

Rack::OAuth2::Server is available under the MIT license.

rack-oauth2-server's People

Contributors

assaf avatar biilmann avatar danielvartanov avatar freegenie avatar jessemiller avatar johnallen3d avatar marcisme avatar msaffitz avatar mulderp avatar mwawrusch avatar oggy avatar rafael avatar spk avatar trumant avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rack-oauth2-server's Issues

Commit 10e246193654da4e9975ea2acfb8e07ed9cfd9fa (pull request 19 in old repo) changes two-legged OAuth access token semantics

The changes introduced in this commit in the old repo changes the semantics for how access tokens are generated in the two-legged OAuth flow.

Prior to this change, a new access token value was generated in the two-legged OAuth flow each time an access token was requested for a given client_id/client_secret pair. After this change, the first time an access token is requested for a given client_id/client_secret pair a new value is generated, and all subsequent requests for an access token for a given client_id/client_secret pair return the same access token value until the access token expires (if configured).

The OAuth 2 v10 spec says of the two-legged flow:

When requesting an access token using the "none" access grant type
(no access grant is included), the client is requesting access to the
protected resources under its control, or those of another resource
owner which has been previously arranged with the authorization
server (the method of which is beyond the scope of this
specification).

This change is passing in the client_id as the "identity" for the two-legged OAuth flow, which isn't technically correct, as there is no resource owner identity involved (at least not explicitly). Typically, the two-legged OAuth flow is used in device-to-server or server-to-server scenarios where a trust mechanism is established outside of OAuth which allows an OAuth client to act on behalf of one or more resource owners without requiring their explicit authorization. In these cases, the client_id/client_secret pair represent the trusted OAuth client, but a single logical OAuth client may actually be N installs/deployments of that single logical client (think mobile apps). In this case, you don't want to share a single access token among all clients, but instead issue separate and distinct access tokens on each request. The association of the access token to a specific resource owner/identity can be done after the fact (for example after user authentication).

I'm proposing that we change this back to always return a new access token for a given client_id/client_secret pair for the two-legged OAuth flow. The logic for re-using access tokens make perfect sense in three-legged OAuth scenarios where there's a specific resource owner in play, I'm not proposing we change that at all.

Let me know if anyone disagrees.

Pull request #9 breaks oauth2-server command line

Seeing this when running register or setup with the oauth2-server command line tool:

$ oauth2-server register --db test
Application name:   test
Application URL:    http://www.example.org
Redirect URI:       http://www.example.org
Scope (space separated names):      read write

Failed to register client: collection names must not start or end with '.'

Seems like the default collection name prefix functionality isn't getting applied.

Re-factor code to support multiple versions of the OAuth2 spec

OAuth2 spec v10 has long been left in the dust, and we should upgrade rack-oauth2-server to honor newer versions of the OAuth2 spec. This means (roughly):

  • Re-factor the code so that the code and tests for a specific version of the spec are isolated.
  • Users can configure which version they want to honor
  • New versions can be added easily.

Admin interface documentation outdated?

Hi.

First of all, thank you for a great gem!

When I followed the configuration doc on how to set up the Admin interface I ran into this error:

.../config/application.rb:54: undefined method `set' for #Module:0xb6980984 (NoMethodError)

I have added this to my application.rb:

Configure OAuth2 Admin interface

config.after_initialize do
  Rack::OAuth2::Server::Admin.set :client_id, "XXX"
  Rack::OAuth2::Server::Admin.set :client_secret, "XXX"
  Rack::OAuth2::Server::Admin.set :scope, %w{read write}
end

If I include Sinatra like this: require 'sinatra', in the application.rb file I get this error:

../config/application.rb:55: private method `set' called for #Module:0xb67023c4 (NoMethodError)

Seems like it's not possible at all to use "set" like it is described in the doc?
How can I get the Admin interface to work?

Best regards,
Thomas.

oauth-webadmin documentation seems to be outdated?

This is the suggested syntax for the config.ru:

Rack::Builder.new do
  map("/oauth/admin") { run Rack::OAuth2::Server::Admin }
  map("/") { run MyApp }
end

This results in the folllowing error for me:
"/Users/mseeger/.rvm/gems/ruby-1.9.3-p0/gems/rack-1.4.0/lib/rack/builder.rb:129:in `to_app': missing run or map statement (RuntimeError)"

Just using:

map("/oauth/admin") { run Rack::OAuth2::Server::Admin }
map("/") { run MyApp }

Seems to work for me though

oauth2-server isn't chopping \n characters from lines read from $stdin.gets

When running oauth2-server, many of the values collected on the command line end up looking like this in the database:

{ "_id" : ObjectId("4d81ce61f1ea7ea4cf999991"), "display_name" : "foo\n", "link" : "http://www.foo.com\n",

Note the \n characters. IO#gets includes the line separator in the line it reads. oauth2-server should chop the string read from $stdin.gets before storing it in the database to remove the trailing \n character

Documentation mentions oauth.scope, doesn't seem to exist

Hey, trying to work out a minimal demo server to see how the flow works, and following along with the documentation I did this:

require 'rubygems'
require 'bundler'
Bundler.setup(:default, :development)

require 'sinatra/base'
require 'rack/oauth2/sinatra'

class MyApp < Sinatra::Base
  attr_reader :current_user

  register Rack::OAuth2::Sinatra

  oauth_required = '/private'

  oauth.database = Mongo::Connection.new['oauth2_server_dev']

  oauth.scope = %w{read write}  # <<-- this line

  oauth.authenticator = lambda do |username,password|

    if username == 'fungus' && password == 'fluffy'
      "user_id_12345"
    else
      nil
    end
  end

  before do
    @current_user = { :id => 'user_id_12345' } if oauth.authenticated?
  end

  get "/monitor/check" do
    'TEST-OK'
  end

  get '/private' do
    "this is ur sekrits, user: #{current_user[:id]}"
  end
end

But when I attempt to run it with rackup i get the following exception:

/Users/slyphon/git/dev/oauth2_server/app.rb:16:in `<class:MyApp>': undefined method `scope=' for #<Rack::OAuth2::Server::Options:0x007fd5928a4e30> (NoMethodError)
        from /Users/slyphon/git/dev/oauth2_server/app.rb:8:in `<top (required)>'
        from /Users/slyphon/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
        from /Users/slyphon/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
        from /Users/slyphon/git/dev/oauth2_server/rackup.ru:3:in `block in <main>'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/builder.rb:51:in `instance_eval'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/builder.rb:51:in `initialize'
        from /Users/slyphon/git/dev/oauth2_server/rackup.ru:1:in `new'
        from /Users/slyphon/git/dev/oauth2_server/rackup.ru:1:in `<main>'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/builder.rb:40:in `eval'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/builder.rb:40:in `parse_file'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/server.rb:200:in `app'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/server.rb:301:in `wrapped_app'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/server.rb:252:in `start'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/server.rb:137:in `start'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/bin/rackup:4:in `<top (required)>'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/bin/rackup:19:in `load'
        from /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/bin/rackup:19:in `<main>'

The line in question is #57 in README.rdoc

assertion_handler: undefined method `[]=' for nil:NilClass (NoMethodError)

oauth.assertion_handler['facebook.com'] = lambda do |client, assertion, requested_scope|
    facebook = URI.parse('https://graph.facebook.com/me?access_token=' + assertion)
    response = Net::HTTP.get_response(facebook)
    puts response
end

leads to the error: undefined method `[]=' for nil:NilClass (NoMethodError)

OAuth admin interface and x_sendfile_header

Hi Assaf!

I've had some trouble with getting the oauth admin interface to work in "production" env. I just got an empty page and nothing helpful in the logs.
After some fiddling I found out that this config line was the culprit:

-- # Specifies the header that your server uses for sending files
config.action_dispatch.x_sendfile_header = "X-Sendfile"

I haven't got a clue as to why this breaks the interface, or if others have encountered the same problems.
But I thought I would mention it and maybe you could enlighten my as to what is the reason for the breakage.

BTW, thanks for a great gem!

Regards,
Thomas.

Improve tests

Hi,

I wanted to ping whether there are needs to improve the tests for this project. Some issues/ideas:

  • add integration tests with e.g. cucumber or capybara
  • split test setups into different files
  • use helper scripts for different frameworks
  • make smaller tests/files
  • move to another test framework (?)

What do you think, am I missing something, or confusing?

Thanks for feedback evt.

Patrick

rack-oauth2-server incompatability with MongoDB 2.x

Hi Assaf.

After upgrading our MongoDB to version 2.x I see this warning in the Rails log when saving a client profile in the oauth admin interface:

Collection#group no longer take a list of parameters. This usage is deprecated and will be remove in v2.0.Check out the new API at http://api.mongodb.org/ruby/current/Mongo/Collection.html#group-instance_method

No matter what I change nothing gets saved at all.
This essentially means the admin interface is broken :o(

I hope you still have time and interest in keeping this wonderful gem up and running...

Best regards,
Thomas Balsløv.

public is no longer used to avoid overloading Module#public, use :public_folder instead

When trying to run very simple app on rack 1.4 or 1.3.6, I get this message:

$ bundle exec rackup
:public is no longer used to avoid overloading Module#public, use :public_folder instead
    from /Users/mseeger/.rvm/gems/ruby-1.9.3-p0/gems/rack-oauth2-server-2.4.1/lib/rack/oauth2/server/admin.rb:81:in `<class:Admin>'

Changing all of the "public" occurrences to "public_folder" seems to fix the problem

.rvmrc ruby version

I propose for this repo to specify the patch version to the ruby one contained in .rvmrc. The latest may change prompting to install the latest version when there's no actual need.

oauth.no_scope in response header causes Rack::Lint::LintError

I'm getting this error in development with rackup (using Lint)

Rack::Lint::LintError: invalid header name: oauth.no_scope

Lint checks header keys to make sure they contain only letters, dashes and underscores. So the period in oauth.no_scope causes an exception.

In server.rb I tried:

scope = Utils.normalize_scope(response[1].delete("oauth.no_scope"))

instead of

scope = Utils.normalize_scope(response[1]["oauth.no_scope"])

and it seems to work by deleting the key from the response.

What is supposed to provide Hash#to_json ?

I'm trying to write a bare-bones oauth2 server, using just sinatra and rack, and I hit this line and blew up.

What is supposed to provide Hash#to_json?

NoMethodError: undefined method `to_json' for #<Hash:0x007fe25c175048>
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-oauth2-server-2.4.1/lib/rack/oauth2/server.rb:393:in `rescue in respond_with_access_token'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-oauth2-server-2.4.1/lib/rack/oauth2/server.rb:352:in `respond_with_access_token'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-oauth2-server-2.4.1/lib/rack/oauth2/server.rb:190:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-protection-1.2.0/lib/rack/protection/xss_header.rb:22:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-protection-1.2.0/lib/rack/protection/path_traversal.rb:16:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-protection-1.2.0/lib/rack/protection/json_csrf.rb:17:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-protection-1.2.0/lib/rack/protection/base.rb:47:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-protection-1.2.0/lib/rack/protection/xss_header.rb:22:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/nulllogger.rb:9:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/head.rb:9:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/sinatra-1.3.2/lib/sinatra/showexceptions.rb:21:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/sinatra-1.3.2/lib/sinatra/base.rb:1334:in `block in call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/sinatra-1.3.2/lib/sinatra/base.rb:1416:in `synchronize'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/sinatra-1.3.2/lib/sinatra/base.rb:1334:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/lint.rb:48:in `_call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/lint.rb:36:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/showexceptions.rb:24:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/commonlogger.rb:20:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/chunked.rb:43:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/content_length.rb:14:in `call'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/connection.rb:80:in `block in pre_process'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/connection.rb:78:in `catch'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/connection.rb:78:in `pre_process'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/connection.rb:53:in `process'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/connection.rb:38:in `receive_data'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:in `run_machine'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/eventmachine-0.12.10/lib/eventmachine.rb:256:in `run'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/backends/base.rb:61:in `start'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/thin-1.3.1/lib/thin/server.rb:159:in `start'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/handler/thin.rb:13:in `run'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/server.rb:265:in `start'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/lib/rack/server.rb:137:in `start'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/gems/rack-1.4.0/bin/rackup:4:in `<top (required)>'
    /Users/slyphon/.rvm/gems/ruby-1.9.3-p0@oauth2_server/bin/rackup:19:in `load'

Admin asking to grant access on every login.

First off, thank you for this oauth2 provider :)

Every time a login to the admin it asks me to grant access, shouldn't it remember I already granted access? Also the grant count never goes up... Any help would be much appreciated.

Many thanks

Expired token and other error handling

I need to have a way to customize handling of expired tokens. Right now this is handled in server.rb:264:


        if token
          begin
            access_token = AccessToken.from_token(token)
            raise InvalidTokenError if access_token.nil? || access_token.revoked
            raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.to_i
            request.env["oauth.access_token"] = token

            request.env["oauth.identity"] = access_token.identity
            access_token.access!
            logger.info "RO2S: Authorized #{access_token.identity}" if logger
          rescue OAuthError=>error
            #5.2.  The WWW-Authenticate Response Header Field
            logger.info "RO2S: HTTP authorization failed #{error.code}" if logger
            return unauthorized(request, error)
          rescue =>ex
            logger.info "RO2S: HTTP authorization failed #{ex.message}" if logger
            return unauthorized(request)
          end

I can think of two ways to easily handle this: (1) A la cancan authorization, we allow the exception to propagate instead of rescuing it, and then it can be caught at a higher layer and managed; (2) insert some kind of handler hook that allows structuring of a response.

Specifically, in this case I am using the authorization in a session with a normal web browser, and so I need to redirect the user to a login page when the token expires.

Any thoughts on better / good ways to solve this?

Rack::OAuth2::Server.token_for returns an expired token

When token is expired, there is no way to re-new it. A request to /oauth/access_token returns the same access token.

An easy way to reproduce the bug:

Rack::OAuth2::Server.token_for(user.id.to_s, client.id.to_s, client.scope, 1.second)
# Wait for 1 second :-)
Rack::OAuth2::Server.token_for(user.id.to_s, client.id.to_s, client.scope)

expected: Either another token or the same token but with updated #expires_at property
actual: The same token with the same properties. An attempt to use this token responds with "The access token has expired."

Assertions for non-JWT tokens, users without access

Use Case:

  1. Mobile device authenticates with Facebook, receives access_token.
  2. Mobile device sends access_token to the server as an assertion
  3. If the server recognizes that Facebook access_token as belonging to an existing user, return our oauth access_token as a normal access_token request; if it doesn't return unauthorized 401.

oauth2-provider (https://github.com/songkick/oauth2-provider) covers this use-case by having an assertion handler:


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

[Edited]: My thought is to do the same thing:

    config.oauth.assertion_handler['my_assertion_type'] = lambda do |client, assertion, scope|
      # ... behave the same as authenticator()
    end

This totally bypasses the register_issuer configuration, which may be seen as a Bad Thing, so please let me know your thoughts.

To avoid mucking anything up with JWT tokens, I'll skip the assertion_handler in this case, but it may be useful later, as I can't see right now how a JWT token would be associated with an local identity unless the original creator knows our identity data. Maybe I am missing something?

Thoughts, better ways to handle, or other comments?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.