rack-oauth2 0.5.1 → 0.6.0.alpha
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/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
|