tpitale-rack-oauth2-server 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +202 -0
- data/Gemfile +16 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +604 -0
- data/Rakefile +90 -0
- data/VERSION +1 -0
- data/bin/oauth2-server +206 -0
- data/lib/rack-oauth2-server.rb +4 -0
- data/lib/rack/oauth2/admin/css/screen.css +347 -0
- data/lib/rack/oauth2/admin/images/loading.gif +0 -0
- data/lib/rack/oauth2/admin/images/oauth-2.png +0 -0
- data/lib/rack/oauth2/admin/js/application.coffee +220 -0
- data/lib/rack/oauth2/admin/js/jquery.js +166 -0
- data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
- data/lib/rack/oauth2/admin/js/protovis-r3.2.js +277 -0
- data/lib/rack/oauth2/admin/js/sammy.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.oauth2.js +142 -0
- data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
- data/lib/rack/oauth2/admin/js/underscore.js +722 -0
- data/lib/rack/oauth2/admin/views/client.tmpl +58 -0
- data/lib/rack/oauth2/admin/views/clients.tmpl +52 -0
- data/lib/rack/oauth2/admin/views/edit.tmpl +80 -0
- data/lib/rack/oauth2/admin/views/index.html +39 -0
- data/lib/rack/oauth2/admin/views/no_access.tmpl +4 -0
- data/lib/rack/oauth2/models.rb +27 -0
- data/lib/rack/oauth2/models/access_grant.rb +54 -0
- data/lib/rack/oauth2/models/access_token.rb +129 -0
- data/lib/rack/oauth2/models/auth_request.rb +61 -0
- data/lib/rack/oauth2/models/client.rb +93 -0
- data/lib/rack/oauth2/rails.rb +105 -0
- data/lib/rack/oauth2/server.rb +458 -0
- data/lib/rack/oauth2/server/admin.rb +250 -0
- data/lib/rack/oauth2/server/errors.rb +104 -0
- data/lib/rack/oauth2/server/helper.rb +147 -0
- data/lib/rack/oauth2/server/practice.rb +79 -0
- data/lib/rack/oauth2/server/railtie.rb +24 -0
- data/lib/rack/oauth2/server/utils.rb +30 -0
- data/lib/rack/oauth2/sinatra.rb +71 -0
- data/rack-oauth2-server.gemspec +24 -0
- data/rails/init.rb +11 -0
- data/test/admin/api_test.rb +228 -0
- data/test/admin/ui_test.rb +38 -0
- data/test/oauth/access_grant_test.rb +276 -0
- data/test/oauth/access_token_test.rb +311 -0
- data/test/oauth/authorization_test.rb +298 -0
- data/test/oauth/server_methods_test.rb +292 -0
- data/test/rails2/app/controllers/api_controller.rb +40 -0
- data/test/rails2/app/controllers/application_controller.rb +2 -0
- data/test/rails2/app/controllers/oauth_controller.rb +17 -0
- data/test/rails2/config/environment.rb +19 -0
- data/test/rails2/config/environments/test.rb +0 -0
- data/test/rails2/config/routes.rb +13 -0
- data/test/rails3/app/controllers/api_controller.rb +40 -0
- data/test/rails3/app/controllers/application_controller.rb +2 -0
- data/test/rails3/app/controllers/oauth_controller.rb +17 -0
- data/test/rails3/config/application.rb +19 -0
- data/test/rails3/config/environment.rb +2 -0
- data/test/rails3/config/routes.rb +12 -0
- data/test/setup.rb +120 -0
- data/test/sinatra/my_app.rb +69 -0
- metadata +145 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module Rack
|
2
|
+
module OAuth2
|
3
|
+
class Server
|
4
|
+
|
5
|
+
# Authorization request. Represents request on behalf of client to access
|
6
|
+
# particular scope. Use this to keep state from incoming authorization
|
7
|
+
# request to grant/deny redirect.
|
8
|
+
class AuthRequest < ActiveRecord::Base
|
9
|
+
belongs_to :client, :class_name => 'Rack::OAuth2::Server::Client'
|
10
|
+
|
11
|
+
# Find AuthRequest from identifier.
|
12
|
+
# def find(request_id)
|
13
|
+
# id = BSON::ObjectId(request_id.to_s)
|
14
|
+
# Server.new_instance self, collection.find_one(id)
|
15
|
+
# rescue BSON::InvalidObjectId
|
16
|
+
# end
|
17
|
+
|
18
|
+
# Create a new authorization request. This holds state, so in addition
|
19
|
+
# to client ID and scope, we need to know the URL to redirect back to
|
20
|
+
# and any state value to pass back in that redirect.
|
21
|
+
def self.create(client, scope, redirect_uri, response_type, state)
|
22
|
+
scope = Utils.normalize_scope(scope) & Utils.normalize_scope(client.scope) # Only allowed scope
|
23
|
+
|
24
|
+
attributes = {
|
25
|
+
:code => Server.secure_random,
|
26
|
+
:client_id => client.id,
|
27
|
+
:scope => scope,
|
28
|
+
:redirect_uri => (client.redirect_uri || redirect_uri),
|
29
|
+
:response_type => response_type,
|
30
|
+
:state => state
|
31
|
+
}
|
32
|
+
|
33
|
+
super(attributes)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Grant access to the specified identity.
|
37
|
+
def grant!(identity)
|
38
|
+
raise ArgumentError, "Must supply a identity" unless identity
|
39
|
+
return if revoked
|
40
|
+
|
41
|
+
if response_type == "code" # Requested authorization code
|
42
|
+
access_grant = AccessGrant.create(identity, client, scope, redirect_uri)
|
43
|
+
update_attributes(:grant_code => access_grant.code, :authorized_at => Time.now)
|
44
|
+
else # Requested access token
|
45
|
+
access_token = AccessToken.get_token_for(identity, client, scope)
|
46
|
+
update_attributes(:access_token => access_token.token, :authorized_at => Time.now)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Deny access.
|
51
|
+
# this seems broken … ?
|
52
|
+
def deny!
|
53
|
+
# self.authorized_at = Time.now.to_i
|
54
|
+
# self.class.collection.update({ :_id=>id }, { :$set=>{ :authorized_at=>authorized_at } })
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Rack
|
2
|
+
module OAuth2
|
3
|
+
class Server
|
4
|
+
|
5
|
+
class Client < ActiveRecord::Base
|
6
|
+
has_many :auth_requests, :dependent => :destroy
|
7
|
+
has_many :access_grants, :dependent => :destroy
|
8
|
+
has_many :access_tokens, :dependent => :destroy
|
9
|
+
|
10
|
+
validates_presence_of :display_name
|
11
|
+
validates_presence_of :link
|
12
|
+
validates_presence_of :code
|
13
|
+
validates_presence_of :secret
|
14
|
+
|
15
|
+
validates_uniqueness_of :display_name
|
16
|
+
validates_uniqueness_of :link
|
17
|
+
validates_uniqueness_of :code
|
18
|
+
validates_uniqueness_of :secret
|
19
|
+
|
20
|
+
before_validation_on_create :assign_code_and_secret
|
21
|
+
|
22
|
+
# Create a new client. Client provides the following properties:
|
23
|
+
# # :display_name -- Name to show (e.g. UberClient)
|
24
|
+
# # :link -- Link to client Web site (e.g. http://uberclient.dot)
|
25
|
+
# # :image_url -- URL of image to show alongside display name
|
26
|
+
# # :redirect_uri -- Registered redirect URI.
|
27
|
+
# # :scope -- List of names the client is allowed to request.
|
28
|
+
# # :notes -- Free form text.
|
29
|
+
#
|
30
|
+
# This method does not validate any of these fields, in fact, you're
|
31
|
+
# not required to set them, use them, or use them as suggested. Using
|
32
|
+
# them as suggested would result in better user experience. Don't ask
|
33
|
+
# how we learned that.
|
34
|
+
# def self.create(args)
|
35
|
+
# unless args[:redirect_uri].blank?
|
36
|
+
# redirect_uri = Server::Utils.parse_redirect_uri(args.delete(:redirect_uri)).to_s
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# scope = Server::Utils.normalize_scope(args[:scope])
|
40
|
+
# args.merge!({:redirect_uri => redirect_uri})
|
41
|
+
#
|
42
|
+
# if args[:id] && args[:secret]
|
43
|
+
# args[:code] = args.delete(:id)
|
44
|
+
# super(args)
|
45
|
+
# else
|
46
|
+
# args[:secret] = Server.secure_random
|
47
|
+
# super(args)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
|
51
|
+
def assign_code_and_secret
|
52
|
+
self.code = Server.secure_random[0,20]
|
53
|
+
self.secret = Server.secure_random
|
54
|
+
end
|
55
|
+
|
56
|
+
def redirect_url=(url)
|
57
|
+
self[:redirect_uri] = Server::Utils.parse_redirect_uri(url).to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
# Lookup client by ID, display name or URL.
|
61
|
+
def self.lookup(field)
|
62
|
+
find_by_id(field) || find_by_code(field) || find_by_display_name(field) || find_by_link(field)
|
63
|
+
end
|
64
|
+
|
65
|
+
# # Counts how many access tokens were granted.
|
66
|
+
# attr_reader :tokens_granted
|
67
|
+
# # Counts how many access tokens were revoked.
|
68
|
+
# attr_reader :tokens_revoked
|
69
|
+
|
70
|
+
# Revoke all authorization requests, access grants and access tokens for
|
71
|
+
# this client. Ward off the evil.
|
72
|
+
def revoke!
|
73
|
+
revoked_at = Time.now
|
74
|
+
update_attribute(:revoked, revoked_at)
|
75
|
+
# can we use the association here
|
76
|
+
AuthRequest.update_all(:revoked=>revoked_at, :client_id=>id)
|
77
|
+
AccessGrant.update_all(:revoked=>revoked_at, :client_id=>id)
|
78
|
+
AccessToken.update_all(:revoked=>revoked_at, :client_id=>id)
|
79
|
+
end
|
80
|
+
|
81
|
+
# def update(args)
|
82
|
+
# redirect_url = Server::Utils.parse_redirect_uri(args[:redirect_uri]).to_s unless args[:redirect_uri].blank?
|
83
|
+
# args.merge!({
|
84
|
+
# :redirect_url => redirect_url,
|
85
|
+
# :scope => Server::Utils.normalize_scope(args.delete(:scope))
|
86
|
+
# })
|
87
|
+
#
|
88
|
+
# update_attributes(args)
|
89
|
+
# end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "rack/oauth2/server"
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module OAuth2
|
5
|
+
|
6
|
+
# Rails support.
|
7
|
+
#
|
8
|
+
# Adds oauth instance method that returns Rack::OAuth2::Helper, see there for
|
9
|
+
# more details.
|
10
|
+
#
|
11
|
+
# Adds oauth_required filter method. Use this filter with actions that require
|
12
|
+
# authentication, and with actions that require client to have a specific
|
13
|
+
# access scope.
|
14
|
+
#
|
15
|
+
# Adds oauth setting you can use to configure the module (e.g. setting
|
16
|
+
# available scope, see example).
|
17
|
+
#
|
18
|
+
# @example config/environment.rb
|
19
|
+
# require "rack/oauth2/rails"
|
20
|
+
#
|
21
|
+
# Rails::Initializer.run do |config|
|
22
|
+
# config.oauth[:scope] = %w{read write}
|
23
|
+
# config.oauth[:authenticator] = lambda do |username, password|
|
24
|
+
# User.authenticated username, password
|
25
|
+
# end
|
26
|
+
# . . .
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @example app/controllers/my_controller.rb
|
30
|
+
# class MyController < ApplicationController
|
31
|
+
#
|
32
|
+
# oauth_required :only=>:show
|
33
|
+
# oauth_required :only=>:update, :scope=>"write"
|
34
|
+
#
|
35
|
+
# . . .
|
36
|
+
#
|
37
|
+
# protected
|
38
|
+
# def current_user
|
39
|
+
# @current_user ||= User.find(oauth.identity) if oauth.authenticated?
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @see Helpers
|
44
|
+
# @see Filters
|
45
|
+
# @see Configuration
|
46
|
+
module Rails
|
47
|
+
|
48
|
+
# Helper methods available to controller instance and views.
|
49
|
+
module Helpers
|
50
|
+
# Returns the OAuth helper.
|
51
|
+
#
|
52
|
+
# @return [Server::Helper]
|
53
|
+
def oauth
|
54
|
+
@oauth ||= Rack::OAuth2::Server::Helper.new(request, response)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Filter that denies access if the request is not authenticated. If you
|
58
|
+
# do not specify a scope, the class method oauth_required will use this
|
59
|
+
# filter; you can set the filter in a parent class and skip it in child
|
60
|
+
# classes that need special handling.
|
61
|
+
def oauth_required
|
62
|
+
head oauth.no_access! unless oauth.authenticated?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Filter methods available in controller.
|
67
|
+
module Filters
|
68
|
+
|
69
|
+
# Adds before filter to require authentication on all the listed paths.
|
70
|
+
# Use the :scope option if client must also have access to that scope.
|
71
|
+
#
|
72
|
+
# @param [Hash] options Accepts before_filter options like :only and
|
73
|
+
# :except, and the :scope option.
|
74
|
+
def oauth_required(options = {})
|
75
|
+
if scope = options.delete(:scope)
|
76
|
+
before_filter options do |controller|
|
77
|
+
if controller.oauth.authenticated?
|
78
|
+
if !controller.oauth.scope.include?(scope)
|
79
|
+
controller.send :head, controller.oauth.no_scope!(scope)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
controller.send :head, controller.oauth.no_access!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
else
|
86
|
+
before_filter :oauth_required, options
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Configuration methods available in config/environment.rb.
|
92
|
+
module Configuration
|
93
|
+
|
94
|
+
# Rack module settings.
|
95
|
+
#
|
96
|
+
# @return [Hash] Settings
|
97
|
+
def oauth
|
98
|
+
@oauth ||= Server::Options.new
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,458 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/oauth2/models"
|
3
|
+
require "rack/oauth2/server/errors"
|
4
|
+
require "rack/oauth2/server/utils"
|
5
|
+
require "rack/oauth2/server/helper"
|
6
|
+
|
7
|
+
|
8
|
+
module Rack
|
9
|
+
module OAuth2
|
10
|
+
|
11
|
+
# Implements an OAuth 2 Authorization Server, based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10
|
12
|
+
class Server
|
13
|
+
|
14
|
+
# Same as gem version number.
|
15
|
+
VERSION = IO.read(::File.expand_path("../../../VERSION", ::File.dirname(__FILE__))).strip
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Return AuthRequest from authorization request handle.
|
19
|
+
#
|
20
|
+
# @param [String] authorization Authorization handle (e.g. from
|
21
|
+
# oauth.authorization)
|
22
|
+
# @return [AuthReqeust]
|
23
|
+
def get_auth_request(authorization)
|
24
|
+
AuthRequest.find_by_code(authorization)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns Client from client identifier.
|
28
|
+
#
|
29
|
+
# @param [String] client_id Client identifier (e.g. from oauth.client.id)
|
30
|
+
# @return [Client]
|
31
|
+
def get_client(client_id)
|
32
|
+
p client_id
|
33
|
+
Client.find_by_code(client_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Registers and returns a new Client. Can also be used to update
|
37
|
+
# existing client registration, by passing identifier (and secret) of
|
38
|
+
# existing client record. That way, your setup script can create a new
|
39
|
+
# client application and run repeatedly without fail.
|
40
|
+
#
|
41
|
+
# @param [Hash] args Arguments for registering client application
|
42
|
+
# @option args [String] :id Client identifier. Use this to update
|
43
|
+
# existing client registration (in combination wih secret)
|
44
|
+
# @option args [String] :secret Client secret. Use this to update
|
45
|
+
# existing client registration.
|
46
|
+
# @option args [String] :display_name Name to show when authorizing
|
47
|
+
# access (e.g. "My Awesome Application")
|
48
|
+
# @option args [String] link Link to client application's Web site
|
49
|
+
# @option args [String] image_url URL of image to show alongside display
|
50
|
+
# name.
|
51
|
+
# @option args [String] redirect_uri Redirect URL: authorization
|
52
|
+
# requests for this client will always redirect back to this URL.
|
53
|
+
# @option args [Array] scope Scope that client application can request
|
54
|
+
# (list of names).
|
55
|
+
# @option args [Array] notes Free form text, for internal use.
|
56
|
+
#
|
57
|
+
# @example Registering new client application
|
58
|
+
# Server.register :display_name=>"My Application",
|
59
|
+
# :link=>"http://example.com", :scope=>%w{read write},
|
60
|
+
# :redirect_uri=>"http://example.com/oauth/callback"
|
61
|
+
# @example Migration using configuration file
|
62
|
+
# config = YAML.load_file(Rails.root + "config/oauth.yml")
|
63
|
+
# Server.register config["id"], config["secret"],
|
64
|
+
# :display_name=>"My Application", :link=>"http://example.com",
|
65
|
+
# :scope=>config["scope"],
|
66
|
+
# :redirect_uri=>"http://example.com/oauth/callback"
|
67
|
+
|
68
|
+
def register(args)
|
69
|
+
if args[:id] && args[:secret] && (client = get_client(args[:id]))
|
70
|
+
fail "Client secret does not match" unless client.secret == args[:secret]
|
71
|
+
client.update args
|
72
|
+
else
|
73
|
+
Client.create(args)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Creates and returns a new access grant. Actually, returns only the
|
78
|
+
# authorization code which you can turn into an access token by
|
79
|
+
# making a request to /oauth/access_token.
|
80
|
+
#
|
81
|
+
# @param [String,Integer] identity User ID, account ID, etc
|
82
|
+
# @param [String] client_id Client identifier
|
83
|
+
# @param [Array, nil] scope Array of string, nil if you want 'em all
|
84
|
+
# @param [Integer, nil] expires How many seconds before access grant
|
85
|
+
# expires (default to 5 minutes)
|
86
|
+
# @return [String] Access grant authorization code
|
87
|
+
def access_grant(identity, client_id, scope = nil, expires = nil)
|
88
|
+
client = get_client(client_id) or fail "No such client"
|
89
|
+
AccessGrant.create(identity, client, scope || client.scope, nil, expires).code
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns AccessToken from token.
|
93
|
+
#
|
94
|
+
# @param [String] token Access token (e.g. from oauth.access_token)
|
95
|
+
# @return [AccessToken]
|
96
|
+
def get_access_token(token)
|
97
|
+
AccessToken.from_token(token)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns AccessToken for the specified identity, client application and
|
101
|
+
# scope. You can use this method to request existing access token, new
|
102
|
+
# token generated if one does not already exists.
|
103
|
+
#
|
104
|
+
# @param [String,Integer] identity Identity, e.g. user ID, account ID
|
105
|
+
# @param [String] client_id Client application identifier
|
106
|
+
# @param [Array, nil] scope Array of names, nil if you want 'em all
|
107
|
+
# @return [String] Access token
|
108
|
+
def token_for(identity, client_id, scope = nil)
|
109
|
+
client = get_client(client_id) or fail "No such client"
|
110
|
+
AccessToken.get_token_for(identity, client, scope || client.scope).token
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns all AccessTokens for an identity.
|
114
|
+
#
|
115
|
+
# @param [String] identity Identity, e.g. user ID, account ID
|
116
|
+
# @return [Array<AccessToken>]
|
117
|
+
def list_access_tokens(identity)
|
118
|
+
AccessToken.from_identity(identity)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
# Options are:
|
124
|
+
# - :access_token_path -- Path for requesting access token. By convention
|
125
|
+
# defaults to /oauth/access_token.
|
126
|
+
# - :authenticator -- For username/password authorization. A block that
|
127
|
+
# receives the credentials and returns identity string (e.g. user ID) or
|
128
|
+
# nil.
|
129
|
+
# - :authorization_types -- Array of supported authorization types.
|
130
|
+
# Defaults to ["code", "token"], and you can change it to just one of
|
131
|
+
# these names.
|
132
|
+
# - :authorize_path -- Path for requesting end-user authorization. By
|
133
|
+
# convention defaults to /oauth/authorize.
|
134
|
+
# - :database -- Mongo::DB instance.
|
135
|
+
# - :host -- Only check requests sent to this host.
|
136
|
+
# - :path -- Only check requests for resources under this path.
|
137
|
+
# - :param_authentication -- If true, supports authentication using
|
138
|
+
# query/form parameters.
|
139
|
+
# - :realm -- Authorization realm that will show up in 401 responses.
|
140
|
+
# Defaults to use the request host name.
|
141
|
+
# - :logger -- The logger to use. Under Rails, defaults to use the Rails
|
142
|
+
# logger. Will use Rack::Logger if available.
|
143
|
+
#
|
144
|
+
# Authenticator is a block that receives either two or four parameters.
|
145
|
+
# The first two are username and password. The other two are the client
|
146
|
+
# identifier and scope. It authenticated, it returns an identity,
|
147
|
+
# otherwise it can return nil or false. For example:
|
148
|
+
# oauth.authenticator = lambda do |username, password|
|
149
|
+
# user = User.find_by_username(username)
|
150
|
+
# user if user && user.authenticated?(password)
|
151
|
+
# end
|
152
|
+
Options = Struct.new(:access_token_path, :authenticator, :authorization_types,
|
153
|
+
:authorize_path, :database, :host, :param_authentication, :path, :realm, :logger)
|
154
|
+
|
155
|
+
def initialize(app, options = Options.new, &authenticator)
|
156
|
+
@app = app
|
157
|
+
@options = options
|
158
|
+
@options.authenticator ||= authenticator
|
159
|
+
@options.access_token_path ||= "/oauth/access_token"
|
160
|
+
@options.authorize_path ||= "/oauth/authorize"
|
161
|
+
@options.authorization_types ||= %w{code token}
|
162
|
+
@options.param_authentication ||= false
|
163
|
+
end
|
164
|
+
|
165
|
+
# @see Options
|
166
|
+
attr_reader :options
|
167
|
+
|
168
|
+
def call(env)
|
169
|
+
request = OAuthRequest.new(env)
|
170
|
+
return @app.call(env) if options.host && options.host != request.host
|
171
|
+
return @app.call(env) if options.path && request.path.index(options.path) != 0
|
172
|
+
|
173
|
+
begin
|
174
|
+
logger = options.logger || env["rack.logger"]
|
175
|
+
|
176
|
+
# 3. Obtaining End-User Authorization
|
177
|
+
# Flow starts here.
|
178
|
+
return request_authorization(request, logger) if request.path == options.authorize_path
|
179
|
+
# 4. Obtaining an Access Token
|
180
|
+
return respond_with_access_token(request, logger) if request.path == options.access_token_path
|
181
|
+
|
182
|
+
# 5. Accessing a Protected Resource
|
183
|
+
if request.authorization
|
184
|
+
# 5.1.1. The Authorization Request Header Field
|
185
|
+
token = request.credentials if request.oauth?
|
186
|
+
elsif options.param_authentication && !request.GET["oauth_verifier"] # Ignore OAuth 1.0 callbacks
|
187
|
+
# 5.1.2. URI Query Parameter
|
188
|
+
# 5.1.3. Form-Encoded Body Parameter
|
189
|
+
token = request.GET["oauth_token"] || request.POST["oauth_token"]
|
190
|
+
token ||= request.GET['access_token'] || request.POST['access_token']
|
191
|
+
end
|
192
|
+
|
193
|
+
if token
|
194
|
+
begin
|
195
|
+
access_token = AccessToken.from_token(token)
|
196
|
+
raise InvalidTokenError if access_token.nil? || access_token.revoked
|
197
|
+
raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.to_i
|
198
|
+
request.env["oauth.access_token"] = token
|
199
|
+
|
200
|
+
request.env["oauth.identity"] = access_token.identity
|
201
|
+
access_token.access!
|
202
|
+
logger.info "RO2S: Authorized #{access_token.identity}" if logger
|
203
|
+
rescue OAuthError=>error
|
204
|
+
# 5.2. The WWW-Authenticate Response Header Field
|
205
|
+
logger.info "RO2S: HTTP authorization failed #{error.code}" if logger
|
206
|
+
return unauthorized(request, error)
|
207
|
+
rescue =>ex
|
208
|
+
logger.info "RO2S: HTTP authorization failed #{ex.message}" if logger
|
209
|
+
return unauthorized(request)
|
210
|
+
end
|
211
|
+
|
212
|
+
# We expect application to use 403 if request has insufficient scope,
|
213
|
+
# and return appropriate WWW-Authenticate header.
|
214
|
+
response = @app.call(env)
|
215
|
+
if response[0] == 403
|
216
|
+
scope = Utils.normalize_scope(response[1]["oauth.no_scope"])
|
217
|
+
challenge = 'OAuth realm="%s", error="insufficient_scope", scope="%s"' % [(options.realm || request.host), scope]
|
218
|
+
response[1]["WWW-Authenticate"] = challenge
|
219
|
+
return response
|
220
|
+
else
|
221
|
+
return response
|
222
|
+
end
|
223
|
+
else
|
224
|
+
response = @app.call(env)
|
225
|
+
if response[1] && response[1].delete("oauth.no_access")
|
226
|
+
logger.debug "RO2S: Unauthorized request" if logger
|
227
|
+
# OAuth access required.
|
228
|
+
return unauthorized(request)
|
229
|
+
elsif response[1] && response[1]["oauth.authorization"]
|
230
|
+
# 3. Obtaining End-User Authorization
|
231
|
+
# Flow ends here.
|
232
|
+
return authorization_response(response, logger)
|
233
|
+
else
|
234
|
+
return response
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
protected
|
241
|
+
|
242
|
+
# Get here for authorization request. Check the request parameters and
|
243
|
+
# redirect with an error if we find any issue. Otherwise, create a new
|
244
|
+
# authorization request, set in oauth.request and pass control to the
|
245
|
+
# application.
|
246
|
+
def request_authorization(request, logger)
|
247
|
+
state = request.GET["state"]
|
248
|
+
begin
|
249
|
+
|
250
|
+
if request.GET["authorization"]
|
251
|
+
auth_request = self.class.get_auth_request(request.GET["authorization"]) rescue nil
|
252
|
+
if !auth_request || auth_request.revoked
|
253
|
+
logger.error "RO2S: Invalid authorization request #{auth_request}" if logger
|
254
|
+
return bad_request("Invalid authorization request")
|
255
|
+
end
|
256
|
+
response_type = auth_request.response_type # Needed for error handling
|
257
|
+
client = auth_request.client
|
258
|
+
# Pass back to application, watch for 403 (deny!)
|
259
|
+
logger.info "RO2S: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope}" if logger
|
260
|
+
request.env["oauth.authorization"] = auth_request.code
|
261
|
+
response = @app.call(request.env)
|
262
|
+
raise AccessDeniedError if response[0] == 403
|
263
|
+
return response
|
264
|
+
|
265
|
+
else
|
266
|
+
|
267
|
+
# 3. Obtaining End-User Authorization
|
268
|
+
begin
|
269
|
+
redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"])
|
270
|
+
rescue InvalidRequestError=>error
|
271
|
+
logger.error "RO2S: Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
|
272
|
+
return bad_request(error.message)
|
273
|
+
end
|
274
|
+
|
275
|
+
# 3. Obtaining End-User Authorization
|
276
|
+
response_type = request.GET["response_type"].to_s # Need this first, for error handling
|
277
|
+
client = get_client(request, :dont_authenticate => true)
|
278
|
+
raise RedirectUriMismatchError unless client.redirect_uri.nil? || client.redirect_uri == redirect_uri.to_s
|
279
|
+
raise UnsupportedResponseTypeError unless options.authorization_types.include?(response_type)
|
280
|
+
requested_scope = Utils.normalize_scope(request.GET["scope"])
|
281
|
+
allowed_scope = Utils.normalize_scope(client.scope)
|
282
|
+
raise InvalidScopeError unless (requested_scope - allowed_scope).empty?
|
283
|
+
# Create object to track authorization request and let application
|
284
|
+
# handle the rest.
|
285
|
+
auth_request = AuthRequest.create(client, requested_scope, redirect_uri.to_s, response_type, state)
|
286
|
+
uri = URI.parse(request.url)
|
287
|
+
uri.query = "authorization=#{auth_request.code}"
|
288
|
+
return redirect_to(uri, 303)
|
289
|
+
end
|
290
|
+
rescue OAuthError=>error
|
291
|
+
logger.error "RO2S: Authorization request error #{error.code}: #{error.message}" if logger
|
292
|
+
params = { :error=>error.code, :error_description=>error.message, :state=>state }
|
293
|
+
if response_type == "token"
|
294
|
+
redirect_uri.fragment = Rack::Utils.build_query(params)
|
295
|
+
else # response type is code, or invalid
|
296
|
+
params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
|
297
|
+
redirect_uri.query = Rack::Utils.build_query(params)
|
298
|
+
end
|
299
|
+
return redirect_to(redirect_uri)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Get here on completion of the authorization. Authorization response in
|
304
|
+
# oauth.response either grants or denies authroization. In either case, we
|
305
|
+
# redirect back with the proper response.
|
306
|
+
def authorization_response(response, logger)
|
307
|
+
status, headers, body = response
|
308
|
+
auth_request = self.class.get_auth_request(headers["oauth.authorization"])
|
309
|
+
redirect_uri = URI.parse(auth_request.redirect_uri)
|
310
|
+
if status == 403
|
311
|
+
auth_request.deny!
|
312
|
+
else
|
313
|
+
auth_request.grant! headers["oauth.identity"]
|
314
|
+
end
|
315
|
+
# 3.1. Authorization Response
|
316
|
+
if auth_request.response_type == "code"
|
317
|
+
if auth_request.grant_code
|
318
|
+
logger.info "RO2S: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
|
319
|
+
params = { :code=>auth_request.grant_code, :scope=>auth_request.scope, :state=>auth_request.state }
|
320
|
+
else
|
321
|
+
logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger
|
322
|
+
params = { :error=>:access_denied, :state=>auth_request.state }
|
323
|
+
end
|
324
|
+
params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
|
325
|
+
redirect_uri.query = Rack::Utils.build_query(params)
|
326
|
+
else # response type if token
|
327
|
+
if auth_request.access_token
|
328
|
+
logger.info "RO2S: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
|
329
|
+
params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope, :state=>auth_request.state }
|
330
|
+
else
|
331
|
+
logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger
|
332
|
+
params = { :error=>:access_denied, :state=>auth_request.state }
|
333
|
+
end
|
334
|
+
redirect_uri.fragment = Rack::Utils.build_query(params)
|
335
|
+
end
|
336
|
+
return redirect_to(redirect_uri)
|
337
|
+
end
|
338
|
+
|
339
|
+
# 4. Obtaining an Access Token
|
340
|
+
def respond_with_access_token(request, logger)
|
341
|
+
return [405, { "Content-Type"=>"application/json" }, ["POST only"]] unless request.post?
|
342
|
+
# 4.2. Access Token Response
|
343
|
+
begin
|
344
|
+
client = get_client(request)
|
345
|
+
case request.POST["grant_type"]
|
346
|
+
when "none"
|
347
|
+
# 4.1 "none" access grant type (i.e. two-legged OAuth flow)
|
348
|
+
requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : Utils.normalize_scope(client.scope)
|
349
|
+
access_token = AccessToken.create_token_for(client, requested_scope)
|
350
|
+
when "authorization_code"
|
351
|
+
# 4.1.1. Authorization Code
|
352
|
+
grant = AccessGrant.from_code(request.POST["code"])
|
353
|
+
p grant
|
354
|
+
raise InvalidGrantError, "Wrong client" unless grant && client == grant.client
|
355
|
+
unless client.redirect_uri.nil? || client.redirect_uri.to_s.empty?
|
356
|
+
raise InvalidGrantError, "Wrong redirect URI" unless grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s
|
357
|
+
end
|
358
|
+
raise InvalidGrantError, "This access grant expired" if grant.expires_at && grant.expires_at <= Time.now.to_i
|
359
|
+
access_token = grant.authorize!
|
360
|
+
when "password"
|
361
|
+
raise UnsupportedGrantType unless options.authenticator
|
362
|
+
# 4.1.2. Resource Owner Password Credentials
|
363
|
+
username, password = request.POST.values_at("username", "password")
|
364
|
+
raise InvalidGrantError, "Missing username/password" unless username && password
|
365
|
+
requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : Utils.normalize_scope(client.scope)
|
366
|
+
allowed_scope = Utils.normalize_scope(client.scope)
|
367
|
+
raise InvalidScopeError unless (requested_scope - allowed_scope).empty?
|
368
|
+
args = [username, password]
|
369
|
+
args << client.id << requested_scope unless options.authenticator.arity == 2
|
370
|
+
identity = options.authenticator.call(*args)
|
371
|
+
raise InvalidGrantError, "Username/password do not match" unless identity
|
372
|
+
access_token = AccessToken.get_token_for(identity, client, requested_scope)
|
373
|
+
else
|
374
|
+
raise UnsupportedGrantType
|
375
|
+
end
|
376
|
+
logger.info "RO2S: Access token #{access_token.token} granted to client #{client.display_name}, identity #{access_token.identity}" if logger
|
377
|
+
response = { :access_token=>access_token.token }
|
378
|
+
response[:scope] = access_token.scope
|
379
|
+
return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, [response.to_json]]
|
380
|
+
# 4.3. Error Response
|
381
|
+
rescue OAuthError=>error
|
382
|
+
logger.error "RO2S: Access token request error #{error.code}: #{error.message}" if logger
|
383
|
+
return unauthorized(request, error) if InvalidClientError === error && request.basic?
|
384
|
+
return [400, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" },
|
385
|
+
[{ :error=>error.code, :error_description=>error.message }.to_json]]
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Returns client from request based on credentials. Raises
|
390
|
+
# InvalidClientError if client doesn't exist or secret doesn't match.
|
391
|
+
def get_client(request, options={})
|
392
|
+
# 2.1 Client Password Credentials
|
393
|
+
if request.basic?
|
394
|
+
client_id, client_secret = request.credentials
|
395
|
+
elsif request.post?
|
396
|
+
client_id, client_secret = request.POST.values_at("client_id", "client_secret")
|
397
|
+
else
|
398
|
+
client_id, client_secret = request.GET.values_at("client_id", "client_secret")
|
399
|
+
end
|
400
|
+
client = self.class.get_client(client_id)
|
401
|
+
raise InvalidClientError if !client
|
402
|
+
unless options[:dont_authenticate]
|
403
|
+
raise InvalidClientError unless client.secret == client_secret
|
404
|
+
end
|
405
|
+
raise InvalidClientError if client.revoked
|
406
|
+
return client
|
407
|
+
end
|
408
|
+
|
409
|
+
# Rack redirect response.
|
410
|
+
# The argument is typically a URI object, and the status should be a 302 or 303.
|
411
|
+
def redirect_to(uri, status = 302)
|
412
|
+
return [status, { "Content-Type"=>"text/plain", "Location"=>uri.to_s }, ["You are being redirected"]]
|
413
|
+
end
|
414
|
+
|
415
|
+
def bad_request(message)
|
416
|
+
return [400, { "Content-Type"=>"text/plain" }, [message]]
|
417
|
+
end
|
418
|
+
|
419
|
+
# Returns WWW-Authenticate header.
|
420
|
+
def unauthorized(request, error = nil)
|
421
|
+
challenge = 'OAuth realm="%s"' % (options.realm || request.host)
|
422
|
+
challenge << ', error="%s", error_description="%s"' % [error.code, error.message] if error
|
423
|
+
return [401, { "WWW-Authenticate"=>challenge }, [error && error.message || ""]]
|
424
|
+
end
|
425
|
+
|
426
|
+
# Wraps Rack::Request to expose Basic and OAuth authentication
|
427
|
+
# credentials.
|
428
|
+
class OAuthRequest < Rack::Request
|
429
|
+
|
430
|
+
AUTHORIZATION_KEYS = %w{HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION}
|
431
|
+
|
432
|
+
# Returns authorization header.
|
433
|
+
def authorization
|
434
|
+
@authorization ||= AUTHORIZATION_KEYS.inject(nil) { |auth, key| auth || @env[key] }
|
435
|
+
end
|
436
|
+
|
437
|
+
# True if authentication scheme is OAuth.
|
438
|
+
def oauth?
|
439
|
+
authorization[/^oauth/i] if authorization
|
440
|
+
end
|
441
|
+
|
442
|
+
# True if authentication scheme is Basic.
|
443
|
+
def basic?
|
444
|
+
authorization[/^basic/i] if authorization
|
445
|
+
end
|
446
|
+
|
447
|
+
# If Basic auth, returns username/password, if OAuth, returns access
|
448
|
+
# token.
|
449
|
+
def credentials
|
450
|
+
basic? ? authorization.gsub(/\n/, "").split[1].unpack("m*").first.split(/:/, 2) :
|
451
|
+
oauth? ? authorization.gsub(/\n/, "").split[1] : nil
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
end
|
456
|
+
|
457
|
+
end
|
458
|
+
end
|