rack-oauth2 1.12.0 → 1.21.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c09e887a3c902ebbbfd1b2a1698d8a4d68d28e18e75616c629dd75e981e2a9d2
4
- data.tar.gz: c0142a2c05df047f0d8f1e9ac11f428983d27a8945d5ae5be213be41b5615e20
3
+ metadata.gz: 7303cf85e66a7fb4a89d66d95b4ad35720ecb95459f9740208328314ea54b157
4
+ data.tar.gz: 061a4a30cbb25212979a37f26e18043cbf71dead3e36981b37f6152fc6899cfd
5
5
  SHA512:
6
- metadata.gz: ab1002bdd363b2edab71e8eeebd9872bca8ea2be6ad2a99875543974a2b85340ddcb42fc4ddf22f779b18429dc48a9d36fd91e9059391b76b537c04293ac2056
7
- data.tar.gz: bdffd308340040287a3a8777cdaaaa9de58873d4d27e080dc719b4715ca53fd413bc726f19382b63cd086ad3dd47ac139a665f0acfcd25d2bb2a5e266d8dafce
6
+ metadata.gz: 5fbabf81d770e80f02614d3b00b0fd9db8a63ed695a5b67b74266eee1f09ec6e7045db009ea7e6ee09af84680699809032ecc64d58caee48305573cd3532b5be
7
+ data.tar.gz: 5bc8cdbdddb9a997560eab574a955ab69d3ad8f9e594554a45d17e077991c2551382c917363c1c09db349abf262f5d9c15a7cfb13c24e56fe27d83cbde62f0f3
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: nov
@@ -0,0 +1,30 @@
1
+ name: Spec
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ spec:
12
+ strategy:
13
+ matrix:
14
+ os: ['ubuntu-20.04']
15
+ ruby-version: ['2.6', '2.7', '3.0', '3.1']
16
+ # ubuntu 22.04 only supports ssl 3 and thus only ruby 3.1
17
+ include:
18
+ - os: 'ubuntu-22.04'
19
+ ruby-version: '3.1'
20
+ runs-on: ${{ matrix.os }}
21
+
22
+ steps:
23
+ - uses: actions/checkout@v3
24
+ - name: Set up Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby-version }}
28
+ bundler-cache: true
29
+ - name: Run Specs
30
+ run: bundle exec rake spec
data/.travis.yml CHANGED
@@ -2,6 +2,7 @@ before_install:
2
2
  - gem install bundler
3
3
 
4
4
  rvm:
5
- - 2.3.6
6
- - 2.4.3
7
- - 2.5.0
5
+ - 2.6.10
6
+ - 2.7.6
7
+ - 3.0.4
8
+ - 3.1.2
data/README.rdoc CHANGED
@@ -28,17 +28,11 @@ http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
28
28
 
29
29
  === Bearer
30
30
 
31
- Running on Heroku
32
- https://rack-oauth2-sample.heroku.com
33
-
34
31
  Source on GitHub
35
32
  https://github.com/nov/rack-oauth2-sample
36
33
 
37
34
  === MAC
38
35
 
39
- Running on Heroku
40
- https://rack-oauth2-sample-mac.heroku.com
41
-
42
36
  Source on GitHub
43
37
  https://github.com/nov/rack-oauth2-sample-mac
44
38
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.12.0
1
+ 1.21.3
@@ -3,7 +3,7 @@ module Rack
3
3
  class Client
4
4
  include AttrRequired, AttrOptional
5
5
  attr_required :identifier
6
- attr_optional :secret, :private_key, :certificate, :redirect_uri, :scheme, :host, :port, :authorization_endpoint, :token_endpoint
6
+ attr_optional :secret, :private_key, :certificate, :redirect_uri, :scheme, :host, :port, :authorization_endpoint, :token_endpoint, :revocation_endpoint
7
7
 
8
8
  def initialize(attributes = {})
9
9
  (required_attributes + optional_attributes).each do |key|
@@ -16,12 +16,12 @@ module Rack
16
16
  end
17
17
 
18
18
  def authorization_uri(params = {})
19
+ params[:redirect_uri] ||= self.redirect_uri
19
20
  params[:response_type] ||= :code
20
21
  params[:response_type] = Array(params[:response_type]).join(' ')
21
22
  params[:scope] = Array(params[:scope]).join(' ')
22
23
  Util.redirect_uri absolute_uri_for(authorization_endpoint), :query, params.merge(
23
- client_id: self.identifier,
24
- redirect_uri: self.redirect_uri
24
+ client_id: self.identifier
25
25
  )
26
26
  end
27
27
 
@@ -69,20 +69,83 @@ module Rack
69
69
  end
70
70
 
71
71
  def access_token!(*args)
72
- headers, params = {}, @grant.as_json
72
+ headers, params, http_client, options = authenticated_context_from(*args)
73
+ params[:scope] = Array(options.delete(:scope)).join(' ') if options[:scope].present?
74
+ params.merge! @grant.as_json
75
+ params.merge! options
76
+ handle_response do
77
+ http_client.post(
78
+ absolute_uri_for(token_endpoint),
79
+ Util.compact_hash(params),
80
+ headers
81
+ )
82
+ end
83
+ end
84
+
85
+ def revoke!(*args)
86
+ headers, params, http_client, options = authenticated_context_from(*args)
87
+
88
+ params.merge! case
89
+ when access_token = options.delete(:access_token)
90
+ {
91
+ token: access_token,
92
+ token_type_hint: :access_token
93
+ }
94
+ when refresh_token = options.delete(:refresh_token)
95
+ {
96
+ token: refresh_token,
97
+ token_type_hint: :refresh_token
98
+ }
99
+ when @grant.is_a?(Grant::RefreshToken)
100
+ {
101
+ token: @grant.refresh_token,
102
+ token_type_hint: :refresh_token
103
+ }
104
+ when options[:token].blank?
105
+ raise ArgumentError, 'One of "token", "access_token" and "refresh_token" is required'
106
+ end
107
+ params.merge! options
108
+
109
+ handle_revocation_response do
110
+ http_client.post(
111
+ absolute_uri_for(revocation_endpoint),
112
+ Util.compact_hash(params),
113
+ headers
114
+ )
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def absolute_uri_for(endpoint)
121
+ _endpoint_ = Util.parse_uri endpoint
122
+ _endpoint_.scheme ||= self.scheme || 'https'
123
+ _endpoint_.host ||= self.host
124
+ _endpoint_.port ||= self.port
125
+ raise 'No Host Info' unless _endpoint_.host
126
+ _endpoint_.to_s
127
+ end
128
+
129
+ def authenticated_context_from(*args)
130
+ headers, params = {}, {}
73
131
  http_client = Rack::OAuth2.http_client
74
132
 
75
133
  # NOTE:
76
- # Using Array#estract_options! for backward compatibility.
134
+ # Using Array#extract_options! for backward compatibility.
77
135
  # Until v1.0.5, the first argument was 'client_auth_method' in scalar.
78
136
  options = args.extract_options!
79
- client_auth_method = args.first || options.delete(:client_auth_method) || :basic
80
-
81
- params[:scope] = Array(options.delete(:scope)).join(' ') if options[:scope].present?
82
- params.merge! options
137
+ client_auth_method = args.first || options.delete(:client_auth_method).try(:to_sym) || :basic
83
138
 
84
139
  case client_auth_method
85
140
  when :basic
141
+ cred = Base64.strict_encode64 [
142
+ Util.www_form_url_encode(identifier),
143
+ Util.www_form_url_encode(secret)
144
+ ].join(':')
145
+ headers.merge!(
146
+ 'Authorization' => "Basic #{cred}"
147
+ )
148
+ when :basic_without_www_form_urlencode
86
149
  cred = ["#{identifier}:#{secret}"].pack('m').tr("\n", '')
87
150
  headers.merge!(
88
151
  'Authorization' => "Basic #{cred}"
@@ -92,9 +155,11 @@ module Rack
92
155
  client_assertion_type: URN::ClientAssertionType::JWT_BEARER
93
156
  )
94
157
  # NOTE: optionally auto-generate client_assertion.
95
- if params[:client_assertion].blank?
158
+ params[:client_assertion] = if options[:client_assertion].present?
159
+ options.delete(:client_assertion)
160
+ else
96
161
  require 'json/jwt'
97
- params[:client_assertion] = JSON::JWT.new(
162
+ JSON::JWT.new(
98
163
  iss: identifier,
99
164
  sub: identifier,
100
165
  aud: absolute_uri_for(token_endpoint),
@@ -119,24 +184,8 @@ module Rack
119
184
  client_secret: secret
120
185
  )
121
186
  end
122
- handle_response do
123
- http_client.post(
124
- absolute_uri_for(token_endpoint),
125
- Util.compact_hash(params),
126
- headers
127
- )
128
- end
129
- end
130
-
131
- private
132
187
 
133
- def absolute_uri_for(endpoint)
134
- _endpoint_ = Util.parse_uri endpoint
135
- _endpoint_.scheme ||= self.scheme || 'https'
136
- _endpoint_.host ||= self.host
137
- _endpoint_.port ||= self.port
138
- raise 'No Host Info' unless _endpoint_.host
139
- _endpoint_.to_s
188
+ [headers, params, http_client, options]
140
189
  end
141
190
 
142
191
  def handle_response
@@ -149,6 +198,16 @@ module Rack
149
198
  end
150
199
  end
151
200
 
201
+ def handle_revocation_response
202
+ response = yield
203
+ case response.status
204
+ when 200..201
205
+ :success
206
+ else
207
+ handle_error_response response
208
+ end
209
+ end
210
+
152
211
  def handle_success_response(response)
153
212
  token_hash = JSON.parse(response.body).with_indifferent_access
154
213
  case (@forced_token_type || token_hash[:token_type]).try(:downcase)
@@ -27,7 +27,7 @@ module Rack
27
27
  response.status = status
28
28
  yield response if block_given?
29
29
  unless response.redirect?
30
- response.header['Content-Type'] = 'application/json'
30
+ response.headers['Content-Type'] = 'application/json'
31
31
  response.write Util.compact_hash(protocol_params).to_json
32
32
  end
33
33
  response.finish
@@ -42,6 +42,7 @@ module Rack
42
42
 
43
43
  class Unauthorized < Error
44
44
  def initialize(error = :unauthorized, description = nil, options = {})
45
+ @skip_www_authenticate = options[:skip_www_authenticate]
45
46
  super 401, error, description, options
46
47
  end
47
48
  end
@@ -5,7 +5,7 @@ module Rack
5
5
  module ResponseExt
6
6
  def redirect?
7
7
  ensure_finish do
8
- @response.redirect?
8
+ super
9
9
  end
10
10
  end
11
11
 
@@ -17,13 +17,13 @@ module Rack
17
17
 
18
18
  def json
19
19
  ensure_finish do
20
- @response.body
20
+ @body
21
21
  end
22
22
  end
23
23
 
24
- def header
24
+ def headers
25
25
  ensure_finish do
26
- @header
26
+ @headers
27
27
  end
28
28
  end
29
29
 
@@ -39,7 +39,7 @@ module Rack
39
39
  end
40
40
 
41
41
  def ensure_finish
42
- @status, @header, @response = finish unless finished?
42
+ @status, @headers, @body = finish unless finished?
43
43
  yield
44
44
  end
45
45
  end
@@ -13,11 +13,11 @@ module Rack
13
13
  def finish
14
14
  super do |response|
15
15
  self.realm ||= DEFAULT_REALM
16
- header = response.header['WWW-Authenticate'] = "#{scheme} realm=\"#{realm}\""
16
+ headers = response.headers['WWW-Authenticate'] = "#{scheme} realm=\"#{realm}\""
17
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?
18
+ headers << ", error=\"#{error}\""
19
+ headers << ", error_description=\"#{description}\"" if description.present?
20
+ headers << ", error_uri=\"#{uri}\"" if uri.present?
21
21
  end
22
22
  end
23
23
  end
@@ -8,7 +8,9 @@ module Rack
8
8
  class Unauthorized < Abstract::Unauthorized
9
9
  def finish
10
10
  super do |response|
11
- response.header['WWW-Authenticate'] = 'Basic realm="OAuth2 Token Endpoint"'
11
+ unless @skip_www_authenticate
12
+ response.headers['WWW-Authenticate'] = 'Basic realm="OAuth2 Token Endpoint"'
13
+ end
12
14
  end
13
15
  end
14
16
  end
@@ -44,16 +44,27 @@ module Rack
44
44
 
45
45
  class Request < Abstract::Request
46
46
  attr_required :grant_type
47
- attr_optional :client_secret
47
+ attr_optional :client_secret, :client_assertion, :client_assertion_type
48
48
 
49
49
  def initialize(env)
50
50
  auth = Rack::Auth::Basic::Request.new(env)
51
51
  if auth.provided? && auth.basic?
52
- @client_id, @client_secret = auth.credentials
52
+ @client_id, @client_secret = auth.credentials.map do |cred|
53
+ Util.www_form_url_decode cred
54
+ end
53
55
  super
54
56
  else
55
57
  super
56
58
  @client_secret = params['client_secret']
59
+ @client_assertion = params['client_assertion']
60
+ @client_assertion_type = params['client_assertion_type']
61
+ if client_assertion.present? && client_assertion_type == URN::ClientAssertionType::JWT_BEARER
62
+ require 'json/jwt'
63
+ @client_id = JSON::JWT.decode(
64
+ client_assertion,
65
+ :skip_verification
66
+ )[:sub] rescue nil
67
+ end
57
68
  end
58
69
  @grant_type = params['grant_type'].to_s
59
70
  end
@@ -69,9 +80,9 @@ module Rack
69
80
  def finish
70
81
  attr_missing!
71
82
  write Util.compact_hash(protocol_params).to_json
72
- header['Content-Type'] = 'application/json'
73
- header['Cache-Control'] = 'no-store'
74
- header['Pragma'] = 'no-cache'
83
+ headers['Content-Type'] = 'application/json'
84
+ headers['Cache-Control'] = 'no-store'
85
+ headers['Pragma'] = 'no-cache'
75
86
  super
76
87
  end
77
88
  end
@@ -3,14 +3,14 @@ module Rack
3
3
  module URN
4
4
  module TokenType
5
5
  JWT = 'urn:ietf:params:oauth:token-type:jwt' # RFC7519
6
- ACCESS_TOKEN = 'urn:ietf:params:oauth:token-type:access-token' # draft-ietf-oauth-token-exchange
7
- REFRESH_TOKEN = 'urn:ietf:params:oauth:token-type:refresh-token' # draft-ietf-oauth-token-exchange
6
+ ACCESS_TOKEN = 'urn:ietf:params:oauth:token-type:access_token' # RFC8693
7
+ REFRESH_TOKEN = 'urn:ietf:params:oauth:token-type:refresh_token' # RFC8693
8
8
  end
9
9
 
10
10
  module GrantType
11
11
  JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer' # RFC7523
12
12
  SAML2_BEARER = 'urn:ietf:params:oauth:grant-type:saml2-bearer' # RFC7522
13
- TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange' # draft-ietf-oauth-token-exchange
13
+ TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange' # RFC8693
14
14
  end
15
15
 
16
16
  module ClientAssertionType
@@ -4,8 +4,12 @@ module Rack
4
4
  module OAuth2
5
5
  module Util
6
6
  class << self
7
- def rfc3986_encode(text)
8
- URI.encode(text, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
7
+ def www_form_url_encode(text)
8
+ URI.encode_www_form_component(text)
9
+ end
10
+
11
+ def www_form_url_decode(text)
12
+ URI.decode_www_form_component(text)
9
13
  end
10
14
 
11
15
  def base64_encode(text)
data/lib/rack/oauth2.rb CHANGED
@@ -43,6 +43,11 @@ module Rack
43
43
  _http_client_ = HTTPClient.new(
44
44
  agent_name: agent_name
45
45
  )
46
+
47
+ # NOTE: httpclient gem seems stopped maintaining root certtificate set, use OS default.
48
+ _http_client_.ssl_config.clear_cert_store
49
+ _http_client_.ssl_config.cert_store.set_default_paths
50
+
46
51
  http_config.try(:call, _http_client_)
47
52
  local_http_config.try(:call, _http_client_) unless local_http_config.nil?
48
53
  _http_client_.request_filter << Debugger::RequestFilter.new if debugging?
data/rack-oauth2.gemspec CHANGED
@@ -7,13 +7,13 @@ Gem::Specification.new do |s|
7
7
  s.email = 'nov@matake.jp'
8
8
  s.extra_rdoc_files = ['LICENSE', 'README.rdoc']
9
9
  s.rdoc_options = ['--charset=UTF-8']
10
- s.homepage = 'http://github.com/nov/rack-oauth2'
10
+ s.homepage = 'https://github.com/nov/rack-oauth2'
11
11
  s.license = 'MIT'
12
12
  s.require_paths = ['lib']
13
13
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
14
  s.files = `git ls-files`.split("\n")
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
- s.add_runtime_dependency 'rack', '< 2.1'
16
+ s.add_runtime_dependency 'rack', '>= 2.1.0'
17
17
  s.add_runtime_dependency 'httpclient'
18
18
  s.add_runtime_dependency 'activesupport'
19
19
  s.add_runtime_dependency 'attr_required'
@@ -23,4 +23,5 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency 'rspec'
24
24
  s.add_development_dependency 'rspec-its'
25
25
  s.add_development_dependency 'webmock'
26
+ s.add_development_dependency 'rexml'
26
27
  end
@@ -1,12 +1,15 @@
1
1
  require 'spec_helper.rb'
2
2
 
3
3
  describe Rack::OAuth2::Client do
4
+ let(:client_id) { 'client_id' }
5
+ let(:client_secret) { 'client_secret' }
4
6
  let :client do
5
7
  Rack::OAuth2::Client.new(
6
- identifier: 'client_id',
7
- secret: 'client_secret',
8
+ identifier: client_id,
9
+ secret: client_secret,
8
10
  host: 'server.example.com',
9
- redirect_uri: 'https://client.example.com/callback'
11
+ redirect_uri: 'https://client.example.com/callback',
12
+ revocation_endpoint: '/oauth2/revoke'
10
13
  )
11
14
  end
12
15
  subject { client }
@@ -15,6 +18,7 @@ describe Rack::OAuth2::Client do
15
18
  its(:secret) { should == 'client_secret' }
16
19
  its(:authorization_endpoint) { should == '/oauth2/authorize' }
17
20
  its(:token_endpoint) { should == '/oauth2/token' }
21
+ its(:revocation_endpoint) { should == '/oauth2/revoke' }
18
22
 
19
23
  context 'when identifier is missing' do
20
24
  it do
@@ -97,6 +101,42 @@ describe Rack::OAuth2::Client do
97
101
  client.access_token!
98
102
  end
99
103
 
104
+ context 'when Basic auth method is used' do
105
+ context 'when client_id is a url' do
106
+ let(:client_id) { 'https://client.example.com'}
107
+
108
+ it 'should be encoded in "application/x-www-form-urlencoded"' do
109
+ mock_response(
110
+ :post,
111
+ 'https://server.example.com/oauth2/token',
112
+ 'tokens/bearer.json',
113
+ request_header: {
114
+ 'Authorization' => 'Basic aHR0cHMlM0ElMkYlMkZjbGllbnQuZXhhbXBsZS5jb206Y2xpZW50X3NlY3JldA=='
115
+ }
116
+ )
117
+ client.access_token!
118
+ end
119
+ end
120
+ end
121
+
122
+ context 'when basic_without_www_form_urlencode method is used' do
123
+ context 'when client_id is a url' do
124
+ let(:client_id) { 'https://client.example.com'}
125
+
126
+ it 'should be encoded in "application/x-www-form-urlencoded"' do
127
+ mock_response(
128
+ :post,
129
+ 'https://server.example.com/oauth2/token',
130
+ 'tokens/bearer.json',
131
+ request_header: {
132
+ 'Authorization' => 'Basic aHR0cHM6Ly9jbGllbnQuZXhhbXBsZS5jb206Y2xpZW50X3NlY3JldA=='
133
+ }
134
+ )
135
+ client.access_token! :basic_without_www_form_urlencode
136
+ end
137
+ end
138
+ end
139
+
100
140
  context 'when jwt_bearer auth method specified' do
101
141
  context 'when client_secret is given' do
102
142
  it 'should be JWT bearer client assertion w/ auto-generated HS256-signed JWT assertion' do
@@ -148,7 +188,7 @@ describe Rack::OAuth2::Client do
148
188
  let :client do
149
189
  Rack::OAuth2::Client.new(
150
190
  identifier: 'client_id',
151
- private_key: OpenSSL::PKey::EC.new('prime256v1').generate_key,
191
+ private_key: OpenSSL::PKey::EC.generate('prime256v1'),
152
192
  host: 'server.example.com',
153
193
  redirect_uri: 'https://client.example.com/callback'
154
194
  )
@@ -408,12 +448,86 @@ describe Rack::OAuth2::Client do
408
448
  end
409
449
  end
410
450
 
451
+ describe '#revoke!' do
452
+ context 'when access_token given' do
453
+ before do
454
+ mock_response(
455
+ :post,
456
+ 'https://server.example.com/oauth2/revoke',
457
+ 'blank',
458
+ status: 200,
459
+ body: {
460
+ token: 'access_token',
461
+ token_type_hint: 'access_token'
462
+ }
463
+ )
464
+ end
465
+ it do
466
+ client.revoke!(access_token: 'access_token').should == :success
467
+ end
468
+ end
469
+
470
+ context 'when refresh_token given' do
471
+ before do
472
+ mock_response(
473
+ :post,
474
+ 'https://server.example.com/oauth2/revoke',
475
+ 'blank',
476
+ status: 200,
477
+ body: {
478
+ token: 'refresh_token',
479
+ token_type_hint: 'refresh_token'
480
+ }
481
+ )
482
+ end
483
+
484
+ context 'as argument' do
485
+ it do
486
+ client.revoke!(refresh_token: 'refresh_token').should == :success
487
+ end
488
+ end
489
+
490
+ context 'as grant' do
491
+ it do
492
+ client.refresh_token = 'refresh_token'
493
+ client.revoke!
494
+ end
495
+ end
496
+ end
497
+
498
+ context 'when error response given' do
499
+ before do
500
+ mock_response(
501
+ :post,
502
+ 'https://server.example.com/oauth2/revoke',
503
+ 'errors/invalid_request.json',
504
+ status: 400
505
+ )
506
+ end
507
+
508
+ it do
509
+ expect do
510
+ client.revoke! access_token: 'access_token'
511
+ end.to raise_error Rack::OAuth2::Client::Error
512
+ end
513
+ end
514
+
515
+ context 'when no token given' do
516
+ it do
517
+ expect do
518
+ client.revoke!
519
+ end.to raise_error ArgumentError
520
+ end
521
+ end
522
+ end
523
+
411
524
  context 'when no host info' do
412
525
  let :client do
413
526
  Rack::OAuth2::Client.new(
414
527
  identifier: 'client_id',
415
528
  secret: 'client_secret',
416
- redirect_uri: 'https://client.example.com/callback'
529
+ redirect_uri: 'https://client.example.com/callback',
530
+ revocation_endpoint: '/oauth2/revoke'
417
531
  )
418
532
  end
419
533
 
@@ -428,5 +542,11 @@ describe Rack::OAuth2::Client do
428
542
  expect { client.access_token! }.to raise_error 'No Host Info'
429
543
  end
430
544
  end
545
+
546
+ describe '#revoke!' do
547
+ it do
548
+ expect { client.revoke! access_token: 'access_token' }.to raise_error 'No Host Info'
549
+ end
550
+ end
431
551
  end
432
552
  end
@@ -23,27 +23,27 @@ describe Rack::OAuth2::Server::Authorize::BadRequest do
23
23
  context 'when protocol_params_location = :query' do
24
24
  before { error.protocol_params_location = :query }
25
25
  it 'should redirect with error in query' do
26
- state, header, response = error.finish
26
+ state, headers, response = error.finish
27
27
  state.should == 302
28
- header["Location"].should == "#{redirect_uri}?error=invalid_request"
28
+ headers["Location"].should == "#{redirect_uri}?error=invalid_request"
29
29
  end
30
30
  end
31
31
 
32
32
  context 'when protocol_params_location = :fragment' do
33
33
  before { error.protocol_params_location = :fragment }
34
34
  it 'should redirect with error in fragment' do
35
- state, header, response = error.finish
35
+ state, headers, response = error.finish
36
36
  state.should == 302
37
- header["Location"].should == "#{redirect_uri}#error=invalid_request"
37
+ headers["Location"].should == "#{redirect_uri}#error=invalid_request"
38
38
  end
39
39
  end
40
40
 
41
41
  context 'otherwise' do
42
42
  before { error.protocol_params_location = :other }
43
43
  it 'should redirect without error' do
44
- state, header, response = error.finish
44
+ state, headers, response = error.finish
45
45
  state.should == 302
46
- header["Location"].should == redirect_uri
46
+ headers["Location"].should == redirect_uri
47
47
  end
48
48
  end
49
49
  end
@@ -12,8 +12,8 @@ describe Rack::OAuth2::Server::Resource::Bearer::Unauthorized do
12
12
 
13
13
  describe '#finish' do
14
14
  it 'should use Bearer scheme' do
15
- status, header, response = error.finish
16
- header['WWW-Authenticate'].should include 'Bearer'
15
+ status, headers, response = error.finish
16
+ headers['WWW-Authenticate'].should include 'Bearer'
17
17
  end
18
18
  end
19
19
  end
@@ -22,29 +22,29 @@ describe Rack::OAuth2::Server::Resource::Bearer do
22
22
 
23
23
  shared_examples_for :authenticated_bearer_request do
24
24
  it 'should be authenticated' do
25
- status, header, response = request
25
+ status, headers, response = request
26
26
  status.should == 200
27
27
  access_token.should == bearer_token
28
28
  end
29
29
  end
30
30
  shared_examples_for :unauthorized_bearer_request do
31
31
  it 'should be unauthorized' do
32
- status, header, response = request
32
+ status, headers, response = request
33
33
  status.should == 401
34
- header['WWW-Authenticate'].should include 'Bearer'
34
+ headers['WWW-Authenticate'].should include 'Bearer'
35
35
  access_token.should be_nil
36
36
  end
37
37
  end
38
38
  shared_examples_for :bad_bearer_request do
39
39
  it 'should be bad_request' do
40
- status, header, response = request
40
+ status, headers, response = request
41
41
  status.should == 400
42
42
  access_token.should be_nil
43
43
  end
44
44
  end
45
45
  shared_examples_for :skipped_authentication_request do
46
46
  it 'should skip OAuth 2.0 authentication' do
47
- status, header, response = request
47
+ status, headers, response = request
48
48
  status.should == 200
49
49
  access_token.should be_nil
50
50
  end
@@ -94,15 +94,15 @@ describe Rack::OAuth2::Server::Resource::Bearer do
94
94
  end
95
95
  end
96
96
  it 'should use specified realm' do
97
- status, header, response = request
98
- header['WWW-Authenticate'].should include "Bearer realm=\"#{realm}\""
97
+ status, headers, response = request
98
+ headers['WWW-Authenticate'].should include "Bearer realm=\"#{realm}\""
99
99
  end
100
100
  end
101
101
 
102
102
  context 'otherwize' do
103
103
  it 'should use default realm' do
104
- status, header, response = request
105
- header['WWW-Authenticate'].should include "Bearer realm=\"#{Rack::OAuth2::Server::Resource::Bearer::DEFAULT_REALM}\""
104
+ status, headers, response = request
105
+ headers['WWW-Authenticate'].should include "Bearer realm=\"#{Rack::OAuth2::Server::Resource::Bearer::DEFAULT_REALM}\""
106
106
  end
107
107
  end
108
108
  end
@@ -7,10 +7,10 @@ describe Rack::OAuth2::Server::Resource::BadRequest do
7
7
 
8
8
  describe '#finish' do
9
9
  it 'should respond in JSON' do
10
- status, header, response = error.finish
10
+ status, headers, response = error.finish
11
11
  status.should == 400
12
- header['Content-Type'].should == 'application/json'
13
- response.body.should == ['{"error":"invalid_request"}']
12
+ headers['Content-Type'].should == 'application/json'
13
+ response.should == ['{"error":"invalid_request"}']
14
14
  end
15
15
  end
16
16
  end
@@ -40,20 +40,20 @@ describe Rack::OAuth2::Server::Resource::Unauthorized do
40
40
 
41
41
  describe '#finish' do
42
42
  it 'should respond in JSON' do
43
- status, header, response = error_with_scheme.finish
43
+ status, headers, response = error_with_scheme.finish
44
44
  status.should == 401
45
- header['Content-Type'].should == 'application/json'
46
- header['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\", error=\"invalid_token\""
47
- response.body.should == ['{"error":"invalid_token"}']
45
+ headers['Content-Type'].should == 'application/json'
46
+ headers['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\", error=\"invalid_token\""
47
+ response.should == ['{"error":"invalid_token"}']
48
48
  end
49
49
 
50
50
  context 'when error_code is not invalid_token' do
51
51
  let(:error) { Rack::OAuth2::Server::Resource::Unauthorized.new(:something) }
52
52
 
53
53
  it 'should have error_code in body but not in WWW-Authenticate header' do
54
- status, header, response = error_with_scheme.finish
55
- header['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\""
56
- response.body.first.should include '"error":"something"'
54
+ status, headers, response = error_with_scheme.finish
55
+ headers['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\""
56
+ response.first.should include '"error":"something"'
57
57
  end
58
58
  end
59
59
 
@@ -61,9 +61,9 @@ describe Rack::OAuth2::Server::Resource::Unauthorized do
61
61
  let(:error) { Rack::OAuth2::Server::Resource::Unauthorized.new }
62
62
 
63
63
  it 'should have error_code in body but not in WWW-Authenticate header' do
64
- status, header, response = error_with_scheme.finish
65
- header['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\""
66
- response.body.first.should == '{"error":"unauthorized"}'
64
+ status, headers, response = error_with_scheme.finish
65
+ headers['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\""
66
+ response.first.should == '{"error":"unauthorized"}'
67
67
  end
68
68
  end
69
69
 
@@ -72,9 +72,9 @@ describe Rack::OAuth2::Server::Resource::Unauthorized do
72
72
  let(:error) { Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(:something, nil, realm: realm) }
73
73
 
74
74
  it 'should use given realm' do
75
- status, header, response = error_with_scheme.finish
76
- header['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\""
77
- response.body.first.should include '"error":"something"'
75
+ status, headers, response = error_with_scheme.finish
76
+ headers['WWW-Authenticate'].should == "Scheme realm=\"#{realm}\""
77
+ response.first.should include '"error":"something"'
78
78
  end
79
79
  end
80
80
  end
@@ -88,10 +88,10 @@ describe Rack::OAuth2::Server::Resource::Forbidden do
88
88
 
89
89
  describe '#finish' do
90
90
  it 'should respond in JSON' do
91
- status, header, response = error.finish
91
+ status, headers, response = error.finish
92
92
  status.should == 403
93
- header['Content-Type'].should == 'application/json'
94
- response.body.should == ['{"error":"insufficient_scope"}']
93
+ headers['Content-Type'].should == 'application/json'
94
+ response.should == ['{"error":"insufficient_scope"}']
95
95
  end
96
96
  end
97
97
 
@@ -99,8 +99,8 @@ describe Rack::OAuth2::Server::Resource::Forbidden do
99
99
  let(:error) { Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(:insufficient_scope, 'Desc', scope: [:scope1, :scope2]) }
100
100
 
101
101
  it 'should have blank WWW-Authenticate header' do
102
- status, header, response = error.finish
103
- response.body.first.should include '"scope":"scope1 scope2"'
102
+ status, headers, response = error.finish
103
+ response.first.should include '"scope":"scope1 scope2"'
104
104
  end
105
105
  end
106
106
  end
@@ -12,8 +12,8 @@ describe Rack::OAuth2::Server::Resource::MAC::Unauthorized do
12
12
 
13
13
  describe '#finish' do
14
14
  it 'should use MAC scheme' do
15
- status, header, response = error.finish
16
- header['WWW-Authenticate'].should =~ /^MAC /
15
+ status, headers, response = error.finish
16
+ headers['WWW-Authenticate'].should =~ /^MAC /
17
17
  end
18
18
  end
19
19
  end
@@ -29,29 +29,29 @@ describe Rack::OAuth2::Server::Resource::MAC do
29
29
 
30
30
  shared_examples_for :non_mac_request do
31
31
  it 'should skip OAuth 2.0 authentication' do
32
- status, header, response = request
32
+ status, headers, response = request
33
33
  status.should == 200
34
34
  access_token.should be_nil
35
35
  end
36
36
  end
37
37
  shared_examples_for :authenticated_mac_request do
38
38
  it 'should be authenticated' do
39
- status, header, response = request
39
+ status, headers, response = request
40
40
  status.should == 200
41
41
  access_token.should == mac_token
42
42
  end
43
43
  end
44
44
  shared_examples_for :unauthorized_mac_request do
45
45
  it 'should be unauthorized' do
46
- status, header, response = request
46
+ status, headers, response = request
47
47
  status.should == 401
48
- header['WWW-Authenticate'].should include 'MAC'
48
+ headers['WWW-Authenticate'].should include 'MAC'
49
49
  access_token.should be_nil
50
50
  end
51
51
  end
52
52
  shared_examples_for :bad_mac_request do
53
53
  it 'should be unauthorized' do
54
- status, header, response = request
54
+ status, headers, response = request
55
55
  status.should == 400
56
56
  access_token.should be_nil
57
57
  end
@@ -60,7 +60,7 @@ describe Rack::OAuth2::Server::Resource::MAC do
60
60
  context 'when no access token is given' do
61
61
  let(:env) { Rack::MockRequest.env_for('/protected_resource') }
62
62
  it 'should skip OAuth 2.0 authentication' do
63
- status, header, response = request
63
+ status, headers, response = request
64
64
  status.should == 200
65
65
  access_token.should be_nil
66
66
  end
@@ -103,15 +103,15 @@ describe Rack::OAuth2::Server::Resource::MAC do
103
103
  end
104
104
  end
105
105
  it 'should use specified realm' do
106
- status, header, response = request
107
- header['WWW-Authenticate'].should include "MAC realm=\"#{realm}\""
106
+ status, headers, response = request
107
+ headers['WWW-Authenticate'].should include "MAC realm=\"#{realm}\""
108
108
  end
109
109
  end
110
110
 
111
111
  context 'otherwize' do
112
112
  it 'should use default realm' do
113
- status, header, response = request
114
- header['WWW-Authenticate'].should include "MAC realm=\"#{Rack::OAuth2::Server::Resource::DEFAULT_REALM}\""
113
+ status, headers, response = request
114
+ headers['WWW-Authenticate'].should include "MAC realm=\"#{Rack::OAuth2::Server::Resource::DEFAULT_REALM}\""
115
115
  end
116
116
  end
117
117
  end
@@ -24,8 +24,8 @@ describe Rack::OAuth2::Server::Token::AuthorizationCode do
24
24
  its(:body) { should include '"token_type":"bearer"' }
25
25
 
26
26
  it 'should prevent to be cached' do
27
- response.header['Cache-Control'].should == 'no-store'
28
- response.header['Pragma'].should == 'no-cache'
27
+ response.headers['Cache-Control'].should == 'no-store'
28
+ response.headers['Pragma'].should == 'no-cache'
29
29
  end
30
30
 
31
31
  [:code].each do |required|
@@ -4,14 +4,19 @@ describe Rack::OAuth2::Server::Token::ClientCredentials do
4
4
  let(:request) { Rack::MockRequest.new app }
5
5
  let(:app) do
6
6
  Rack::OAuth2::Server::Token.new do |request, response|
7
+ unless request.client_id == client_id && request.client_secret == client_secret
8
+ request.invalid_client!
9
+ end
7
10
  response.access_token = Rack::OAuth2::AccessToken::Bearer.new(access_token: 'access_token')
8
11
  end
9
12
  end
13
+ let(:client_id) { 'client_id '}
14
+ let(:client_secret) { 'client_secret' }
10
15
  let(:params) do
11
16
  {
12
17
  grant_type: 'client_credentials',
13
- client_id: 'client_id',
14
- client_secret: 'client_secret'
18
+ client_id: client_id,
19
+ client_secret: client_secret
15
20
  }
16
21
  end
17
22
  subject { request.post('/', params: params) }
@@ -20,4 +25,29 @@ describe Rack::OAuth2::Server::Token::ClientCredentials do
20
25
  its(:content_type) { should == 'application/json' }
21
26
  its(:body) { should include '"access_token":"access_token"' }
22
27
  its(:body) { should include '"token_type":"bearer"' }
28
+
29
+ context 'basic auth' do
30
+ let(:params) do
31
+ { grant_type: 'client_credentials' }
32
+ end
33
+ let(:encoded_creds) do
34
+ Base64.strict_encode64([
35
+ Rack::OAuth2::Util.www_form_url_encode(client_id),
36
+ Rack::OAuth2::Util.www_form_url_encode(client_secret)
37
+ ].join(':'))
38
+ end
39
+ subject do
40
+ request.post('/',
41
+ {params: params, 'HTTP_AUTHORIZATION' => "Basic #{encoded_creds}"})
42
+ end
43
+
44
+ its(:status) { should == 200 }
45
+
46
+ context 'compliance with RFC6749 sec 2.3.1' do
47
+ let(:client_id) { 'client: yes/please!' }
48
+ let(:client_secret) { 'terrible:secret:of:space' }
49
+
50
+ its(:status) { should == 200 }
51
+ end
52
+ end
23
53
  end
@@ -7,10 +7,10 @@ describe Rack::OAuth2::Server::Token::BadRequest do
7
7
 
8
8
  describe '#finish' do
9
9
  it 'should respond in JSON' do
10
- status, header, response = error.finish
10
+ status, headers, response = error.finish
11
11
  status.should == 400
12
- header['Content-Type'].should == 'application/json'
13
- response.body.should == ['{"error":"invalid_request"}']
12
+ headers['Content-Type'].should == 'application/json'
13
+ response.should == ['{"error":"invalid_request"}']
14
14
  end
15
15
  end
16
16
  end
@@ -22,11 +22,11 @@ describe Rack::OAuth2::Server::Token::Unauthorized do
22
22
 
23
23
  describe '#finish' do
24
24
  it 'should respond in JSON' do
25
- status, header, response = error.finish
25
+ status, headers, response = error.finish
26
26
  status.should == 401
27
- header['Content-Type'].should == 'application/json'
28
- header['WWW-Authenticate'].should == 'Basic realm="OAuth2 Token Endpoint"'
29
- response.body.should == ['{"error":"invalid_request"}']
27
+ headers['Content-Type'].should == 'application/json'
28
+ headers['WWW-Authenticate'].should == 'Basic realm="OAuth2 Token Endpoint"'
29
+ response.should == ['{"error":"invalid_request"}']
30
30
  end
31
31
  end
32
32
  end
@@ -74,4 +74,4 @@ describe Rack::OAuth2::Server::Token::ErrorMethods do
74
74
  end
75
75
  end
76
76
  end
77
- end
77
+ end
@@ -28,9 +28,9 @@ describe Rack::OAuth2::Server::Token do
28
28
  )
29
29
  end
30
30
  it 'should fail with unsupported_grant_type' do
31
- status, header, response = app.call(env)
31
+ status, headers, response = app.call(env)
32
32
  status.should == 400
33
- response.body.first.should include '"error":"invalid_request"'
33
+ response.first.should include '"error":"invalid_request"'
34
34
  end
35
35
  end
36
36
 
@@ -43,7 +43,7 @@ describe Rack::OAuth2::Server::Token do
43
43
  )
44
44
  end
45
45
  it 'should ignore duplicates' do
46
- status, header, response = app.call(env)
46
+ status, headers, response = app.call(env)
47
47
  status.should == 200
48
48
  end
49
49
  end
@@ -71,6 +71,60 @@ describe Rack::OAuth2::Server::Token do
71
71
  end
72
72
  end
73
73
 
74
+ context 'when client_id is given via JWT client assertion' do
75
+ before do
76
+ require 'json/jwt'
77
+ params[:client_assertion] = JSON::JWT.new(
78
+ sub: params[:client_id]
79
+ # NOTE: actual client_assertion should have more claims.
80
+ ).sign('client_secret').to_s
81
+ params[:client_assertion_type] = Rack::OAuth2::URN::ClientAssertionType::JWT_BEARER
82
+ params.delete(:client_id)
83
+ end
84
+
85
+ context 'when client_assertion is invalid JWT' do
86
+ before do
87
+ params[:client_assertion] = 'invalid-jwt'
88
+ end
89
+ its(:status) { should == 400 }
90
+ its(:content_type) { should == 'application/json' }
91
+ its(:body) { should include '"error":"invalid_request"' }
92
+ end
93
+
94
+ context 'when client_assertion_type is missing' do
95
+ before do
96
+ params.delete(:client_assertion_type)
97
+ end
98
+ its(:status) { should == 400 }
99
+ its(:content_type) { should == 'application/json' }
100
+ its(:body) { should include '"error":"invalid_request"' }
101
+ end
102
+
103
+ context 'when client_assertion_type is unknown' do
104
+ before do
105
+ params[:client_assertion_type] = 'unknown'
106
+ end
107
+ its(:status) { should == 400 }
108
+ its(:content_type) { should == 'application/json' }
109
+ its(:body) { should include '"error":"invalid_request"' }
110
+ end
111
+
112
+ context 'when client_assertion issuer is different from client_id' do
113
+ before do
114
+ params[:client_id] = 'another_client_id'
115
+ end
116
+ its(:status) { should == 400 }
117
+ its(:content_type) { should == 'application/json' }
118
+ its(:body) { should include '"error":"invalid_request"' }
119
+ end
120
+
121
+ context 'otherwise' do
122
+ its(:status) { should == 200 }
123
+ its(:content_type) { should == 'application/json' }
124
+ its(:body) { should include '"access_token":"access_token"' }
125
+ end
126
+ end
127
+
74
128
  Rack::OAuth2::Server::Token::ErrorMethods::DEFAULT_DESCRIPTION.each do |error, default_message|
75
129
  status = if error == :invalid_client
76
130
  401
@@ -87,7 +141,22 @@ describe Rack::OAuth2::Server::Token do
87
141
  its(:content_type) { should == 'application/json' }
88
142
  its(:body) { should include "\"error\":\"#{error}\"" }
89
143
  its(:body) { should include "\"error_description\":\"#{default_message}\"" }
144
+ if error == :invalid_client
145
+ its(:headers) { should include 'WWW-Authenticate' }
146
+ end
147
+ end
148
+ end
149
+
150
+ context 'when skip_www_authenticate option is specified on invalid_client' do
151
+ let(:app) do
152
+ Rack::OAuth2::Server::Token.new do |request, response|
153
+ request.invalid_client!(
154
+ Rack::OAuth2::Server::Token::ErrorMethods::DEFAULT_DESCRIPTION[:invalid_client],
155
+ skip_www_authenticate: true
156
+ )
157
+ end
90
158
  end
159
+ its(:headers) { should_not include 'WWW-Authenticate' }
91
160
  end
92
161
 
93
162
  context 'when responding' do
@@ -9,9 +9,14 @@ describe Rack::OAuth2::Util do
9
9
  'http://client.example.com/callback'
10
10
  end
11
11
 
12
- describe '.rfc3986_encode' do
13
- subject { util.rfc3986_encode '=+ .-/' }
14
- it { should == '%3D%2B%20.-%2F' }
12
+ describe '.www_form_url_encode' do
13
+ subject { util.www_form_url_encode '=+ .-/' }
14
+ it { should == '%3D%2B+.-%2F' }
15
+ end
16
+
17
+ describe '.www_form_urldecode' do
18
+ subject { util.www_form_url_decode '%3D%2B+.-%2F' }
19
+ it { should == '=+ .-/' }
15
20
  end
16
21
 
17
22
  describe '.base64_encode' do
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-oauth2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
4
+ version: 1.21.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - nov matake
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-25 00:00:00.000000000 Z
11
+ date: 2022-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "<"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.1'
19
+ version: 2.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "<"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.1'
26
+ version: 2.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: httpclient
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +150,20 @@ dependencies:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rexml
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  description: OAuth 2.0 Server & Client Library. Both Bearer and MAC token type are
154
168
  supported.
155
169
  email: nov@matake.jp
@@ -160,6 +174,8 @@ extra_rdoc_files:
160
174
  - README.rdoc
161
175
  files:
162
176
  - ".document"
177
+ - ".github/FUNDING.yml"
178
+ - ".github/workflows/spec.yml"
163
179
  - ".gitignore"
164
180
  - ".rspec"
165
181
  - ".travis.yml"
@@ -281,11 +297,11 @@ files:
281
297
  - spec/rack/oauth2/server/token_spec.rb
282
298
  - spec/rack/oauth2/util_spec.rb
283
299
  - spec/spec_helper.rb
284
- homepage: http://github.com/nov/rack-oauth2
300
+ homepage: https://github.com/nov/rack-oauth2
285
301
  licenses:
286
302
  - MIT
287
303
  metadata: {}
288
- post_install_message:
304
+ post_install_message:
289
305
  rdoc_options:
290
306
  - "--charset=UTF-8"
291
307
  require_paths:
@@ -301,8 +317,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
301
317
  - !ruby/object:Gem::Version
302
318
  version: '0'
303
319
  requirements: []
304
- rubygems_version: 3.0.3
305
- signing_key:
320
+ rubygems_version: 3.3.7
321
+ signing_key:
306
322
  specification_version: 4
307
323
  summary: OAuth 2.0 Server & Client Library - Both Bearer and MAC token type are supported
308
324
  test_files: