rack-oauth2 0.5.1 → 0.6.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/lib/rack/oauth2.rb +2 -1
- data/lib/rack/oauth2/access_token.rb +30 -0
- data/lib/rack/oauth2/access_token/bearer.rb +29 -0
- data/lib/rack/oauth2/access_token/mac.rb +109 -0
- data/lib/rack/oauth2/access_token/mac/body_hash.rb +15 -0
- data/lib/rack/oauth2/access_token/mac/signature.rb +49 -0
- data/lib/rack/oauth2/access_token/mac/verifier.rb +43 -0
- data/lib/rack/oauth2/server/authorize/code.rb +0 -1
- data/lib/rack/oauth2/server/resource.rb +55 -1
- data/lib/rack/oauth2/server/resource/bearer.rb +12 -39
- data/lib/rack/oauth2/server/resource/bearer/error.rb +5 -60
- data/lib/rack/oauth2/server/resource/error.rb +81 -0
- data/lib/rack/oauth2/server/resource/mac.rb +39 -0
- data/lib/rack/oauth2/server/resource/mac/error.rb +24 -0
- data/lib/rack/oauth2/server/token.rb +3 -10
- data/lib/rack/oauth2/server/token/refresh_token.rb +0 -2
- data/lib/rack/oauth2/util.rb +10 -0
- data/spec/fake_response/facebook_token_response.txt +1 -0
- data/spec/fake_response/resources/fake.txt +1 -0
- data/spec/helpers/time.rb +19 -0
- data/spec/rack/oauth2/access_token/bearer_spec.rb +43 -0
- data/spec/rack/oauth2/access_token/mac/verifier_spec.rb +23 -0
- data/spec/rack/oauth2/access_token/mac_spec.rb +163 -0
- data/spec/rack/oauth2/access_token_spec.rb +48 -0
- data/spec/rack/oauth2/client_spec.rb +18 -6
- data/spec/rack/oauth2/server/resource/bearer/error_spec.rb +9 -87
- data/spec/rack/oauth2/server/resource/bearer_spec.rb +40 -69
- data/spec/rack/oauth2/server/resource/error_spec.rb +147 -0
- data/spec/rack/oauth2/server/resource/mac/error_spec.rb +52 -0
- data/spec/rack/oauth2/server/resource/mac_spec.rb +92 -0
- data/spec/rack/oauth2/server/resource_spec.rb +23 -0
- data/spec/rack/oauth2/server/token/authorization_code_spec.rb +1 -2
- data/spec/rack/oauth2/server/token/client_credentials_spec.rb +1 -2
- data/spec/rack/oauth2/server/token/password_spec.rb +1 -2
- data/spec/rack/oauth2/server/token/refresh_token_spec.rb +1 -2
- data/spec/rack/oauth2/server/token_spec.rb +1 -2
- data/spec/rack/oauth2/util_spec.rb +10 -0
- data/spec/spec_helper.rb +1 -0
- metadata +38 -6
@@ -0,0 +1,81 @@
|
|
1
|
+
module Rack
|
2
|
+
module OAuth2
|
3
|
+
module Server
|
4
|
+
class Resource
|
5
|
+
class BadRequest < Abstract::BadRequest
|
6
|
+
end
|
7
|
+
|
8
|
+
class Unauthorized < Abstract::Unauthorized
|
9
|
+
def scheme
|
10
|
+
raise 'Define me!'
|
11
|
+
end
|
12
|
+
|
13
|
+
def finish
|
14
|
+
super do |response|
|
15
|
+
self.realm ||= DEFAULT_REALM
|
16
|
+
header = response.header['WWW-Authenticate'] = "#{scheme} realm=\"#{realm}\""
|
17
|
+
if ErrorMethods::DEFAULT_DESCRIPTION.keys.include?(error)
|
18
|
+
header << " error=\"#{error}\""
|
19
|
+
header << " error_description=\"#{description}\"" if description.present?
|
20
|
+
header << " error_uri=\"#{uri}\"" if uri.present?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Forbidden < Abstract::Forbidden
|
27
|
+
attr_accessor :scope
|
28
|
+
|
29
|
+
def initialize(error = :forbidden, description = nil, options = {})
|
30
|
+
super
|
31
|
+
@scope = options[:scope]
|
32
|
+
end
|
33
|
+
|
34
|
+
def protocol_params
|
35
|
+
super.merge(:scope => Array(scope).join(' '))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module ErrorMethods
|
40
|
+
DEFAULT_DESCRIPTION = {
|
41
|
+
:invalid_request => "The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.",
|
42
|
+
:invalid_token => "The access token provided is expired, revoked, malformed or invalid for other reasons.",
|
43
|
+
:insufficient_scope => "The request requires higher privileges than provided by the access token."
|
44
|
+
}
|
45
|
+
|
46
|
+
def self.included(klass)
|
47
|
+
DEFAULT_DESCRIPTION.each do |error, default_description|
|
48
|
+
error_method = case error
|
49
|
+
when :invalid_request
|
50
|
+
:bad_request!
|
51
|
+
when :insufficient_scope
|
52
|
+
:forbidden!
|
53
|
+
else
|
54
|
+
:unauthorized!
|
55
|
+
end
|
56
|
+
klass.class_eval <<-ERROR
|
57
|
+
def #{error}!(description = "#{default_description}", options = {})
|
58
|
+
#{error_method} :#{error}, description, options
|
59
|
+
end
|
60
|
+
ERROR
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def bad_request!(error, description = nil, options = {})
|
65
|
+
raise BadRequest.new(error, description, options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def unauthorized!(error = nil, description = nil, options = {})
|
69
|
+
raise 'Define me!'
|
70
|
+
end
|
71
|
+
|
72
|
+
def forbidden!(error, description = nil, options = {})
|
73
|
+
raise Forbidden.new(error, description, options)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Request.send :include, ErrorMethods
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Rack
|
2
|
+
module OAuth2
|
3
|
+
module Server
|
4
|
+
class Resource
|
5
|
+
class MAC < Resource
|
6
|
+
def call(env)
|
7
|
+
self.request = Request.new(env)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
class Request < Resource::Request
|
14
|
+
attr_reader :timestamp, :nonce, :body_hash, :signature
|
15
|
+
|
16
|
+
def setup!
|
17
|
+
auth_params = @auth_header.params.split(' ').inject({}) do |auth_params, pair|
|
18
|
+
key, value = pair.scan(/^(.*)=\"(.*)\"/).flatten
|
19
|
+
auth_params.merge!(key => value)
|
20
|
+
end.with_indifferent_access
|
21
|
+
@access_token = auth_params[:token]
|
22
|
+
@timestamp = auth_params[:timestamp]
|
23
|
+
@nonce = auth_params[:nonce]
|
24
|
+
@body_hash = auth_params[:bodyhash]
|
25
|
+
@signature = auth_params[:signature]
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def oauth2?
|
30
|
+
@auth_header.provided? && @auth_header.scheme == :mac
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'rack/oauth2/server/resource/mac/error'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rack
|
2
|
+
module OAuth2
|
3
|
+
module Server
|
4
|
+
class Resource
|
5
|
+
class MAC
|
6
|
+
class Unauthorized < Resource::Unauthorized
|
7
|
+
def scheme
|
8
|
+
:MAC
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ErrorMethods
|
13
|
+
include Resource::ErrorMethods
|
14
|
+
def unauthorized!(error = nil, description = nil, options = {})
|
15
|
+
raise Unauthorized.new(error, description, options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Request.send :include, ErrorMethods
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -46,23 +46,16 @@ module Rack
|
|
46
46
|
end
|
47
47
|
|
48
48
|
class Response < Abstract::Response
|
49
|
-
attr_required :access_token
|
50
|
-
attr_optional :refresh_token, :expires_in, :scope
|
49
|
+
attr_required :access_token
|
51
50
|
|
52
51
|
def protocol_params
|
53
|
-
|
54
|
-
:access_token => access_token,
|
55
|
-
:refresh_token => refresh_token,
|
56
|
-
:token_type => token_type,
|
57
|
-
:expires_in => expires_in,
|
58
|
-
:scope => Array(scope).join(' ')
|
59
|
-
}
|
52
|
+
access_token.token_response
|
60
53
|
end
|
61
54
|
|
62
55
|
def finish
|
56
|
+
attr_missing!
|
63
57
|
write Util.compact_hash(protocol_params).to_json
|
64
58
|
header['Content-Type'] = "application/json"
|
65
|
-
attr_missing!
|
66
59
|
super
|
67
60
|
end
|
68
61
|
end
|
@@ -3,7 +3,6 @@ module Rack
|
|
3
3
|
module Server
|
4
4
|
class Token
|
5
5
|
class RefreshToken < Abstract::Handler
|
6
|
-
|
7
6
|
def call(env)
|
8
7
|
@request = Request.new(env)
|
9
8
|
@response = Response.new(request)
|
@@ -20,7 +19,6 @@ module Rack
|
|
20
19
|
attr_missing!
|
21
20
|
end
|
22
21
|
end
|
23
|
-
|
24
22
|
end
|
25
23
|
end
|
26
24
|
end
|
data/lib/rack/oauth2/util.rb
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
module OAuth2
|
3
5
|
module Util
|
4
6
|
class << self
|
7
|
+
def rfc3986_encode(text)
|
8
|
+
URI.encode(text, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
9
|
+
end
|
10
|
+
|
11
|
+
def base64_encode(text)
|
12
|
+
Base64.encode64(text).gsub(/\n/, '')
|
13
|
+
end
|
14
|
+
|
5
15
|
def compact_hash(hash)
|
6
16
|
hash.reject do |key, value|
|
7
17
|
value.blank?
|
@@ -0,0 +1 @@
|
|
1
|
+
access_token=access_token&expires_in=3600
|
@@ -0,0 +1 @@
|
|
1
|
+
fake
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Time
|
2
|
+
class << self
|
3
|
+
def now_with_fixed_time
|
4
|
+
if @fixed_time
|
5
|
+
@fixed_time.dup
|
6
|
+
else
|
7
|
+
now_without_fixed_time
|
8
|
+
end
|
9
|
+
end
|
10
|
+
alias_method_chain :now, :fixed_time
|
11
|
+
|
12
|
+
def fix(time = Time.now)
|
13
|
+
@fixed_time = time
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
@fixed_time = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rack::OAuth2::AccessToken::Bearer do
|
4
|
+
let :token do
|
5
|
+
Rack::OAuth2::AccessToken::Bearer.new(
|
6
|
+
:access_token => 'access_token'
|
7
|
+
)
|
8
|
+
end
|
9
|
+
let(:resource_endpoint) { 'https://server.example.com/resources/fake' }
|
10
|
+
|
11
|
+
[:get, :delete].each do |method|
|
12
|
+
before do
|
13
|
+
fake_response(method, resource_endpoint, 'resources/fake.txt')
|
14
|
+
end
|
15
|
+
|
16
|
+
describe method.to_s.upcase do
|
17
|
+
it 'should have Bearer Authorization header' do
|
18
|
+
RestClient.should_receive(method).with(
|
19
|
+
resource_endpoint,
|
20
|
+
:HTTP_AUTHORIZATION => 'Bearer access_token'
|
21
|
+
)
|
22
|
+
token.send method, resource_endpoint
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
[:post, :put].each do |method|
|
28
|
+
before do
|
29
|
+
fake_response(method, resource_endpoint, 'resources/fake.txt')
|
30
|
+
end
|
31
|
+
|
32
|
+
describe method.to_s.upcase do
|
33
|
+
it 'should have Bearer Authorization header' do
|
34
|
+
RestClient.should_receive(method).with(
|
35
|
+
resource_endpoint,
|
36
|
+
{:key => :value},
|
37
|
+
{:HTTP_AUTHORIZATION => 'Bearer access_token'}
|
38
|
+
)
|
39
|
+
token.send method, resource_endpoint, {:key => :value}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rack::OAuth2::AccessToken::MAC::Verifier do
|
4
|
+
let(:verifier) { Rack::OAuth2::AccessToken::MAC::Verifier.new(:algorithm => algorithm) }
|
5
|
+
subject { verifier }
|
6
|
+
|
7
|
+
context 'when "hmac-sha-1" is specified' do
|
8
|
+
let(:algorithm) { 'hmac-sha-1' }
|
9
|
+
its(:hash_generator) { should be_instance_of OpenSSL::Digest::SHA1 }
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'when "hmac-sha-256" is specified' do
|
13
|
+
let(:algorithm) { 'hmac-sha-256' }
|
14
|
+
its(:hash_generator) { should be_instance_of OpenSSL::Digest::SHA256 }
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'otherwise' do
|
18
|
+
let(:algorithm) { 'invalid' }
|
19
|
+
it do
|
20
|
+
expect { verifier.send(:hash_generator) }.should raise_error(StandardError, 'Unsupported Algorithm')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rack::OAuth2::AccessToken::MAC do
|
4
|
+
let :token do
|
5
|
+
Rack::OAuth2::AccessToken::MAC.new(
|
6
|
+
:access_token => 'access_token',
|
7
|
+
:secret => 'secret',
|
8
|
+
:algorithm => 'hmac-sha-256'
|
9
|
+
)
|
10
|
+
end
|
11
|
+
let(:resource_endpoint) { 'https://server.example.com/resources/fake' }
|
12
|
+
subject { token }
|
13
|
+
|
14
|
+
its(:secret) { should == 'secret' }
|
15
|
+
its(:algorithm) { should == 'hmac-sha-256' }
|
16
|
+
its(:token_response) do
|
17
|
+
should == {
|
18
|
+
:token_type => :mac,
|
19
|
+
:access_token => 'access_token',
|
20
|
+
:secret => 'secret',
|
21
|
+
:algorithm => 'hmac-sha-256',
|
22
|
+
:expires_in => nil,
|
23
|
+
:refresh_token => nil,
|
24
|
+
:scope => ''
|
25
|
+
}
|
26
|
+
end
|
27
|
+
its(:generate_nonce) { should be_a String }
|
28
|
+
|
29
|
+
describe 'HTTP methods' do
|
30
|
+
before do
|
31
|
+
token.should_receive(:generate_nonce).and_return("51e74de734c05613f37520872e68db5f")
|
32
|
+
end
|
33
|
+
|
34
|
+
describe :GET do
|
35
|
+
let(:resource_endpoint) { 'https://server.example.com/resources/fake?key=value' }
|
36
|
+
it 'should have MAC Authorization header' do
|
37
|
+
Time.fix(Time.at(1302361200)) do
|
38
|
+
RestClient.should_receive(:get).with(
|
39
|
+
resource_endpoint,
|
40
|
+
:HTTP_AUTHORIZATION => "MAC token=\"access_token\" timestamp=\"1302361200\" nonce=\"51e74de734c05613f37520872e68db5f\" signature=\"yYDSkZMrEbOOqj0anHNLA9ougNA+lxU0zmPiMSPtmJ8=\""
|
41
|
+
)
|
42
|
+
token.get resource_endpoint
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe :POST do
|
48
|
+
it 'should have MAC Authorization header' do
|
49
|
+
Time.fix(Time.at(1302361200)) do
|
50
|
+
RestClient.should_receive(:post).with(
|
51
|
+
resource_endpoint,
|
52
|
+
{:key => :value},
|
53
|
+
{:HTTP_AUTHORIZATION => "MAC token=\"access_token\" timestamp=\"1302361200\" nonce=\"51e74de734c05613f37520872e68db5f\" bodyhash=\"Vj8DVxGNBe8UXWvd8pZswj6Gyo8vAT+RXlZa/fCfeiM=\" signature=\"xRvIiA+rmjhPjULVpyCCgiHEsOkLEHZik4ZaB+cyqgk=\""}
|
54
|
+
)
|
55
|
+
token.post resource_endpoint, :key => :value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe :PUT do
|
61
|
+
it 'should have MAC Authorization header' do
|
62
|
+
Time.fix(Time.at(1302361200)) do
|
63
|
+
RestClient.should_receive(:put).with(
|
64
|
+
resource_endpoint,
|
65
|
+
{:key => :value},
|
66
|
+
{:HTTP_AUTHORIZATION => "MAC token=\"access_token\" timestamp=\"1302361200\" nonce=\"51e74de734c05613f37520872e68db5f\" bodyhash=\"Vj8DVxGNBe8UXWvd8pZswj6Gyo8vAT+RXlZa/fCfeiM=\" signature=\"2lWgkUCtD9lNBlDi5fe9eVDwEwbxfLGAqjgykaSV1ww=\""}
|
67
|
+
)
|
68
|
+
token.put resource_endpoint, :key => :value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe :DELETE do
|
74
|
+
it 'should have MAC Authorization header' do
|
75
|
+
Time.fix(Time.at(1302361200)) do
|
76
|
+
RestClient.should_receive(:delete).with(
|
77
|
+
resource_endpoint,
|
78
|
+
:HTTP_AUTHORIZATION => "MAC token=\"access_token\" timestamp=\"1302361200\" nonce=\"51e74de734c05613f37520872e68db5f\" signature=\"PX2GhHuo5yYNEs51e4Zlllw8itQ4Te0v+6ZuRCK7k+s=\""
|
79
|
+
)
|
80
|
+
token.delete resource_endpoint
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'verify!' do
|
87
|
+
let(:request) { Rack::OAuth2::Server::Resource::MAC::Request.new(env) }
|
88
|
+
|
89
|
+
context 'when no body_hash is given' do
|
90
|
+
let(:env) do
|
91
|
+
Rack::MockRequest.env_for(
|
92
|
+
'/protected_resources',
|
93
|
+
'HTTP_AUTHORIZATION' => "MAC token=\"access_token\" timestamp=\"1302361200\" nonce=\"51e74de734c05613f37520872e68db5f\" signature=\"#{signature}\""
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'when signature is valid' do
|
98
|
+
let(:signature) { 'jaOwJ1eEjabkiRJJ4hdOeVvCGfiCHgeb9NDSrkM0ka4=' }
|
99
|
+
it do
|
100
|
+
Time.fix(Time.at(1302361200)) do
|
101
|
+
token.verify!(request.setup!).should == :verified
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'otherwise' do
|
107
|
+
let(:signature) { 'invalid' }
|
108
|
+
it do
|
109
|
+
expect { token.verify!(request.setup!) }.should raise_error(
|
110
|
+
Rack::OAuth2::AccessToken::MAC::Verifier::VerificationFailed,
|
111
|
+
'Signature Invalid'
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when body_hash is given' do
|
118
|
+
let(:env) do
|
119
|
+
Rack::MockRequest.env_for(
|
120
|
+
'/protected_resources',
|
121
|
+
:method => :POST,
|
122
|
+
:params => {
|
123
|
+
:key1 => 'value1'
|
124
|
+
},
|
125
|
+
'HTTP_AUTHORIZATION' => "MAC token=\"access_token\" timestamp=\"1302361200\" nonce=\"51e74de734c05613f37520872e68db5f\" bodyhash=\"#{body_hash}\" signature=\"#{signature}\""
|
126
|
+
)
|
127
|
+
end
|
128
|
+
let(:signature) { 'invalid' }
|
129
|
+
|
130
|
+
context 'when body_hash is invalid' do
|
131
|
+
let(:body_hash) { 'invalid' }
|
132
|
+
it do
|
133
|
+
expect { token.verify!(request.setup!) }.should raise_error(
|
134
|
+
Rack::OAuth2::AccessToken::MAC::Verifier::VerificationFailed,
|
135
|
+
'BodyHash Invalid'
|
136
|
+
)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'when body_hash is valid' do
|
141
|
+
let(:body_hash) { 'TPzUbFn1S16mpfmwXCi1L+8oZHRxlLX9/D1ZwAV781o=' }
|
142
|
+
|
143
|
+
context 'when signature is valid' do
|
144
|
+
let(:signature) { 'ZSicF/YCg5bdDef103/NLGeuhH7ylHwrnYUUMcCz12A=' }
|
145
|
+
it do
|
146
|
+
Time.fix(Time.at(1302361200)) do
|
147
|
+
token.verify!(request.setup!).should == :verified
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'otherwise' do
|
153
|
+
it do
|
154
|
+
expect { token.verify!(request.setup!) }.should raise_error(
|
155
|
+
Rack::OAuth2::AccessToken::MAC::Verifier::VerificationFailed,
|
156
|
+
'Signature Invalid'
|
157
|
+
)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|