oidc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +28 -0
  4. data/CHANGELOG.md +4 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +46 -0
  8. data/Rakefile +12 -0
  9. data/lib/oidc/access_token/mtls.rb +9 -0
  10. data/lib/oidc/access_token.rb +45 -0
  11. data/lib/oidc/client/registrar.rb +186 -0
  12. data/lib/oidc/client.rb +43 -0
  13. data/lib/oidc/connect_object.rb +52 -0
  14. data/lib/oidc/discovery/provider/config/resource.rb +39 -0
  15. data/lib/oidc/discovery/provider/config/response.rb +112 -0
  16. data/lib/oidc/discovery/provider/config.rb +20 -0
  17. data/lib/oidc/discovery/provider.rb +34 -0
  18. data/lib/oidc/discovery.rb +8 -0
  19. data/lib/oidc/exception.rb +39 -0
  20. data/lib/oidc/jwtnizable.rb +14 -0
  21. data/lib/oidc/request_object/claimable.rb +54 -0
  22. data/lib/oidc/request_object/id_token.rb +8 -0
  23. data/lib/oidc/request_object/user_info.rb +7 -0
  24. data/lib/oidc/request_object.rb +37 -0
  25. data/lib/oidc/response_object/id_token.rb +99 -0
  26. data/lib/oidc/response_object/user_info/address.rb +10 -0
  27. data/lib/oidc/response_object/user_info.rb +65 -0
  28. data/lib/oidc/response_object.rb +8 -0
  29. data/lib/oidc/version.rb +5 -0
  30. data/lib/oidc.rb +98 -0
  31. data/lib/rack/oauth2/server/authorize/error_with_connect_ext.rb +34 -0
  32. data/lib/rack/oauth2/server/authorize/extension/code_and_id_token.rb +40 -0
  33. data/lib/rack/oauth2/server/authorize/extension/code_and_id_token_and_token.rb +36 -0
  34. data/lib/rack/oauth2/server/authorize/extension/id_token.rb +40 -0
  35. data/lib/rack/oauth2/server/authorize/extension/id_token_and_token.rb +36 -0
  36. data/lib/rack/oauth2/server/authorize/request_with_connect_params.rb +26 -0
  37. data/lib/rack/oauth2/server/id_token_response.rb +24 -0
  38. data/oidc.gemspec +46 -0
  39. data/sig/omniauth_oidc.rbs +4 -0
  40. metadata +252 -0
@@ -0,0 +1,112 @@
1
+ module Oidc
2
+ module Discovery
3
+ module Provider
4
+ class Config
5
+ class Response
6
+ include ActiveModel::Validations, AttrRequired, AttrOptional
7
+
8
+ cattr_accessor :metadata_attributes
9
+ attr_reader :raw
10
+ attr_accessor :expected_issuer
11
+ uri_attributes = {
12
+ required: [
13
+ :issuer,
14
+ :authorization_endpoint,
15
+ :jwks_uri
16
+ ],
17
+ optional: [
18
+ :token_endpoint,
19
+ :userinfo_endpoint,
20
+ :registration_endpoint,
21
+ :end_session_endpoint,
22
+ :service_documentation,
23
+ :check_session_iframe,
24
+ :op_policy_uri,
25
+ :op_tos_uri
26
+ ]
27
+ }
28
+ attr_required(*(uri_attributes[:required] + [
29
+ :response_types_supported,
30
+ :subject_types_supported,
31
+ :id_token_signing_alg_values_supported
32
+ ]))
33
+ attr_optional(*(uri_attributes[:optional] + [
34
+ :scopes_supported,
35
+ :response_modes_supported,
36
+ :grant_types_supported,
37
+ :acr_values_supported,
38
+ :id_token_encryption_alg_values_supported,
39
+ :id_token_encryption_enc_values_supported,
40
+ :userinfo_signing_alg_values_supported,
41
+ :userinfo_encryption_alg_values_supported,
42
+ :userinfo_encryption_enc_values_supported,
43
+ :request_object_signing_alg_values_supported,
44
+ :request_object_encryption_alg_values_supported,
45
+ :request_object_encryption_enc_values_supported,
46
+ :token_endpoint_auth_methods_supported,
47
+ :token_endpoint_auth_signing_alg_values_supported,
48
+ :display_values_supported,
49
+ :claim_types_supported,
50
+ :claims_supported,
51
+ :claims_locales_supported,
52
+ :ui_locales_supported,
53
+ :claims_parameter_supported,
54
+ :request_parameter_supported,
55
+ :request_uri_parameter_supported,
56
+ :require_request_uri_registration
57
+ ]))
58
+
59
+ validates(*required_attributes, presence: true)
60
+ validates(*uri_attributes.values.flatten, url: true, allow_nil: true)
61
+ validates :issuer, with: :validate_issuer_matching
62
+
63
+ def initialize(hash)
64
+ (required_attributes + optional_attributes).each do |key|
65
+ self.send "#{key}=", hash[key]
66
+ end
67
+ @raw = hash
68
+ end
69
+
70
+ def as_json(options = {})
71
+ validate!
72
+ (required_attributes + optional_attributes).inject({}) do |hash, _attr_|
73
+ value = self.send _attr_
74
+ hash.merge! _attr_ => value unless value.nil?
75
+ hash
76
+ end
77
+ end
78
+
79
+ def validate!
80
+ valid? or raise ValidationFailed.new(self)
81
+ end
82
+
83
+ def jwks
84
+ @jwks ||= Oidc.http_client.get(jwks_uri).body.with_indifferent_access
85
+ JSON::JWK::Set.new @jwks[:keys]
86
+ end
87
+
88
+ def jwk(kid)
89
+ @jwks ||= {}
90
+ @jwks[kid] ||= JSON::JWK::Set::Fetcher.fetch(jwks_uri, kid: kid)
91
+ end
92
+
93
+ def public_keys
94
+ @public_keys ||= jwks.collect(&:to_key)
95
+ end
96
+
97
+ private
98
+
99
+ def validate_issuer_matching
100
+ if expected_issuer.present? && issuer != expected_issuer
101
+ if Oidc.validate_discovery_issuer
102
+ errors.add :issuer, 'mismatch'
103
+ else
104
+ Oidc.logger.warn 'ignoring issuer mismach.'
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,20 @@
1
+ module Oidc
2
+ module Discovery
3
+ module Provider
4
+ class Config
5
+ def self.discover!(identifier, cache_options = {})
6
+ uri = URI.parse(identifier)
7
+ Resource.new(uri).discover!(cache_options).tap do |response|
8
+ response.expected_issuer = identifier
9
+ response.validate!
10
+ end
11
+ rescue SWD::Exception, ValidationFailed => e
12
+ raise DiscoveryFailed.new(e.message)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require 'oidc/discovery/provider/config/resource'
20
+ require 'oidc/discovery/provider/config/response'
@@ -0,0 +1,34 @@
1
+ module Oidc
2
+ module Discovery
3
+ module Provider
4
+ module Issuer
5
+ REL_VALUE = 'http://openid.net/specs/connect/1.0/issuer'
6
+
7
+ def issuer
8
+ self.link_for(REL_VALUE)[:href]
9
+ end
10
+ end
11
+
12
+ def self.discover!(identifier)
13
+ resource = case identifier
14
+ when /^acct:/, /https?:\/\//
15
+ identifier
16
+ when /@/
17
+ "acct:#{identifier}"
18
+ else
19
+ "https://#{identifier}"
20
+ end
21
+ response = WebFinger.discover!(
22
+ resource,
23
+ rel: Issuer::REL_VALUE
24
+ )
25
+ response.extend Issuer
26
+ response
27
+ rescue WebFinger::Exception => e
28
+ raise DiscoveryFailed.new(e.message)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ require 'oidc/discovery/provider/config'
@@ -0,0 +1,8 @@
1
+ module Oidc
2
+ module Discovery
3
+ class InvalidIdentifier < Exception; end
4
+ class DiscoveryFailed < Exception; end
5
+ end
6
+ end
7
+
8
+ require 'oidc/discovery/provider'
@@ -0,0 +1,39 @@
1
+ module Oidc
2
+ class Exception < StandardError; end
3
+
4
+ class ValidationFailed < Exception
5
+ attr_reader :object
6
+
7
+ def initialize(object)
8
+ super object.errors.full_messages.to_sentence
9
+ @object = object
10
+ end
11
+ end
12
+
13
+ class HttpError < Exception
14
+ attr_accessor :status, :response
15
+ def initialize(status, message = nil, response = nil)
16
+ super message
17
+ @status = status
18
+ @response = response
19
+ end
20
+ end
21
+
22
+ class BadRequest < HttpError
23
+ def initialize(message = nil, response = nil)
24
+ super 400, message, response
25
+ end
26
+ end
27
+
28
+ class Unauthorized < HttpError
29
+ def initialize(message = nil, response = nil)
30
+ super 401, message, response
31
+ end
32
+ end
33
+
34
+ class Forbidden < HttpError
35
+ def initialize(message = nil, response = nil)
36
+ super 403, message, response
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ module Oidc
2
+ module JWTnizable
3
+ def to_jwt(key, algorithm = :RS256, &block)
4
+ as_jwt(key, algorithm, &block).to_s
5
+ end
6
+
7
+ def as_jwt(key, algorithm = :RS256, &block)
8
+ token = JSON::JWT.new as_json
9
+ yield token if block_given?
10
+ token = token.sign key, algorithm if algorithm != :none
11
+ token
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,54 @@
1
+ module Oidc
2
+ class RequestObject
3
+ module Claimable
4
+ def self.included(klass)
5
+ klass.send :attr_optional, :claims
6
+ end
7
+
8
+ def initialize(attributes = {})
9
+ super
10
+ if claims.present?
11
+ _claims_ = {}
12
+ claims.each do |key, value|
13
+ _claims_[key] = case value
14
+ when :optional, :voluntary
15
+ {
16
+ essential: false
17
+ }
18
+ when :required, :essential
19
+ {
20
+ essential: true
21
+ }
22
+ else
23
+ value
24
+ end
25
+ end
26
+ self.claims = _claims_.with_indifferent_access
27
+ end
28
+ end
29
+
30
+ def as_json(options = {})
31
+ keys = claims.try(:keys)
32
+ hash = super
33
+ Array(keys).each do |key|
34
+ hash[:claims][key] ||= nil
35
+ end
36
+ hash
37
+ end
38
+
39
+ def required?(claim)
40
+ accessible?(claim) && claims[claim].is_a?(Hash) && claims[claim][:essential]
41
+ end
42
+ alias_method :essential?, :required?
43
+
44
+ def optional?(claim)
45
+ accessible?(claim) && !required?(claim)
46
+ end
47
+ alias_method :voluntary?, :optional?
48
+
49
+ def accessible?(claim)
50
+ claims.try(:include?, claim)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,8 @@
1
+ module Oidc
2
+ class RequestObject
3
+ class IdToken < ConnectObject
4
+ include Claimable
5
+ attr_optional :max_age
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Oidc
2
+ class RequestObject
3
+ class UserInfo < ConnectObject
4
+ include Claimable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ module Oidc
2
+ class RequestObject < ConnectObject
3
+ include JWTnizable
4
+
5
+ attr_optional :client_id, :response_type, :redirect_uri, :scope, :state, :nonce, :display, :prompt, :userinfo, :id_token
6
+ validate :require_at_least_one_attributes
7
+
8
+ undef :id_token=
9
+ def id_token=(attributes = {})
10
+ @id_token = IdToken.new(attributes) if attributes.present?
11
+ end
12
+
13
+ undef :userinfo=
14
+ def userinfo=(attributes = {})
15
+ @userinfo = UserInfo.new(attributes) if attributes.present?
16
+ end
17
+
18
+ def as_json(options = {})
19
+ super.with_indifferent_access
20
+ end
21
+
22
+ class << self
23
+ def decode(jwt_string, key = nil)
24
+ new JSON::JWT.decode(jwt_string, key)
25
+ end
26
+
27
+ def fetch(request_uri, key = nil)
28
+ jwt_string = Oidc.http_client.get(request_uri).body
29
+ decode jwt_string, key
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ require 'oidc/request_object/claimable'
36
+ require 'oidc/request_object/id_token'
37
+ require 'oidc/request_object/user_info'
@@ -0,0 +1,99 @@
1
+ module Oidc
2
+ class ResponseObject
3
+ class IdToken < ConnectObject
4
+ class InvalidToken < Exception; end
5
+ class ExpiredToken < InvalidToken; end
6
+ class InvalidIssuer < InvalidToken; end
7
+ class InvalidNonce < InvalidToken; end
8
+ class InvalidAudience < InvalidToken; end
9
+
10
+ attr_required :iss, :sub, :aud, :exp, :iat
11
+ attr_optional :acr, :amr, :azp, :jti, :sid, :auth_time, :nonce, :sub_jwk, :at_hash, :c_hash, :s_hash
12
+ attr_accessor :access_token, :code, :state
13
+ alias_method :subject, :sub
14
+ alias_method :subject=, :sub=
15
+
16
+ def initialize(attributes = {})
17
+ super
18
+ (all_attributes - [:aud, :exp, :iat, :auth_time, :sub_jwk]).each do |key|
19
+ self.send "#{key}=", self.send(key).try(:to_s)
20
+ end
21
+ self.auth_time = auth_time.to_i unless auth_time.nil?
22
+ end
23
+
24
+ def verify!(expected = {})
25
+ raise ExpiredToken.new('Invalid ID token: Expired token') unless exp.to_i > Time.now.to_i
26
+ raise InvalidIssuer.new('Invalid ID token: Issuer does not match') unless iss == expected[:issuer]
27
+ raise InvalidNonce.new('Invalid ID Token: Nonce does not match') unless nonce == expected[:nonce]
28
+
29
+ # aud(ience) can be a string or an array of strings
30
+ unless Array(aud).include?(expected[:audience] || expected[:client_id])
31
+ raise InvalidAudience.new('Invalid ID token: Audience does not match')
32
+ end
33
+
34
+ true
35
+ end
36
+
37
+ include JWTnizable
38
+ def to_jwt(key, algorithm = :RS256, &block)
39
+ hash_length = algorithm.to_s[2, 3].to_i
40
+ if access_token
41
+ token = case access_token
42
+ when Rack::OAuth2::AccessToken
43
+ access_token.access_token
44
+ else
45
+ access_token
46
+ end
47
+ self.at_hash = left_half_hash_of token, hash_length
48
+ end
49
+ if code
50
+ self.c_hash = left_half_hash_of code, hash_length
51
+ end
52
+ if state
53
+ self.s_hash = left_half_hash_of state, hash_length
54
+ end
55
+ super
56
+ end
57
+
58
+ private
59
+
60
+ def left_half_hash_of(string, hash_length)
61
+ digest = OpenSSL::Digest.new("SHA#{hash_length}").digest string
62
+ Base64.urlsafe_encode64 digest[0, hash_length / (2 * 8)], padding: false
63
+ end
64
+
65
+ class << self
66
+ def decode(jwt_string, key_or_config)
67
+ case key_or_config
68
+ when :self_issued
69
+ decode_self_issued jwt_string
70
+ when Oidc::Discovery::Provider::Config::Response
71
+ jwt = JSON::JWT.decode jwt_string, :skip_verification
72
+ jwt.verify! key_or_config.jwk(jwt.kid)
73
+ new jwt
74
+ else
75
+ new JSON::JWT.decode jwt_string, key_or_config
76
+ end
77
+ end
78
+
79
+ def decode_self_issued(jwt_string)
80
+ jwt = JSON::JWT.decode jwt_string, :skip_verification
81
+ jwk = JSON::JWK.new jwt[:sub_jwk]
82
+ raise InvalidToken.new('Missing sub_jwk') if jwk.blank?
83
+ raise InvalidToken.new('Invalid subject') unless jwt[:sub] == jwk.thumbprint
84
+ jwt.verify! jwk
85
+ new jwt
86
+ end
87
+
88
+ def self_issued(attributes = {})
89
+ attributes[:sub_jwk] ||= JSON::JWK.new attributes.delete(:public_key)
90
+ _attributes_ = {
91
+ iss: 'https://self-issued.me',
92
+ sub: JSON::JWK.new(attributes[:sub_jwk]).thumbprint
93
+ }.merge(attributes)
94
+ new _attributes_
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,10 @@
1
+ module Oidc
2
+ class ResponseObject
3
+ class UserInfo
4
+ class Address < ConnectObject
5
+ attr_optional :formatted, :street_address, :locality, :region, :postal_code, :country
6
+ validate :require_at_least_one_attributes
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,65 @@
1
+ module Oidc
2
+ class ResponseObject
3
+ class UserInfo < ConnectObject
4
+ attr_optional(
5
+ :sub,
6
+ :name,
7
+ :given_name,
8
+ :family_name,
9
+ :middle_name,
10
+ :nickname,
11
+ :preferred_username,
12
+ :profile,
13
+ :picture,
14
+ :website,
15
+ :email,
16
+ :email_verified,
17
+ :gender,
18
+ :birthdate,
19
+ :zoneinfo,
20
+ :locale,
21
+ :phone_number,
22
+ :phone_number_verified,
23
+ :address,
24
+ :updated_at
25
+ )
26
+ alias_method :subject, :sub
27
+ alias_method :subject=, :sub=
28
+
29
+ validates :email_verified, :phone_number_verified, allow_nil: true, inclusion: {in: [true, false]}
30
+ validates :zoneinfo, allow_nil: true, inclusion: {in: TZInfo::TimezoneProxy.all.collect(&:name)}
31
+ validates :profile, :picture, :website, allow_nil: true, url: true
32
+ validates :email, allow_nil: true, email: true
33
+ validates :updated_at, allow_nil: true, numericality: {only_integer: true}
34
+ validate :validate_address
35
+ validate :require_at_least_one_attributes
36
+ # TODO: validate locale
37
+
38
+ def initialize(attributes = {})
39
+ super
40
+ (all_attributes - [:email_verified, :phone_number_verified, :address, :updated_at]).each do |key|
41
+ self.send "#{key}=", self.send(key).try(:to_s)
42
+ end
43
+ self.updated_at = updated_at.try(:to_i)
44
+ end
45
+
46
+ def validate_address
47
+ errors.add :address, address.errors.full_messages.join(', ') if address.present? && !address.valid?
48
+ end
49
+
50
+ undef :address=
51
+ def address=(hash_or_address)
52
+ @address = case hash_or_address
53
+ when Hash
54
+ Address.new hash_or_address
55
+ when Address
56
+ hash_or_address
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ Dir[File.dirname(__FILE__) + '/user_info/*.rb'].each do |file|
64
+ require file
65
+ end
@@ -0,0 +1,8 @@
1
+ module Oidc
2
+ class ResponseObject < ConnectObject
3
+ end
4
+ end
5
+
6
+ Dir[File.dirname(__FILE__) + '/response_object/*.rb'].each do |file|
7
+ require file
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oidc
4
+ VERSION = "0.0.1"
5
+ end
data/lib/oidc.rb ADDED
@@ -0,0 +1,98 @@
1
+ require 'json'
2
+ require 'logger'
3
+ require 'faraday'
4
+ require 'faraday/follow_redirects'
5
+ require 'swd'
6
+ require 'webfinger'
7
+ require 'active_model'
8
+ require 'tzinfo'
9
+ require 'validate_url'
10
+ require 'email_validator/strict'
11
+ require 'mail'
12
+ require 'attr_required'
13
+ require 'attr_optional'
14
+ require 'json/jwt'
15
+ require 'rack/oauth2'
16
+ require 'rack/oauth2/server/authorize/error_with_connect_ext'
17
+ require 'rack/oauth2/server/authorize/request_with_connect_params'
18
+ require 'rack/oauth2/server/id_token_response'
19
+
20
+ module Oidc
21
+ def self.logger
22
+ @@logger
23
+ end
24
+ def self.logger=(logger)
25
+ @@logger = logger
26
+ end
27
+ self.logger = Logger.new(STDOUT)
28
+ self.logger.progname = 'Oidc'
29
+
30
+ @sub_protocols = [
31
+ SWD,
32
+ WebFinger,
33
+ Rack::OAuth2
34
+ ]
35
+ def self.debugging?
36
+ @@debugging
37
+ end
38
+ def self.debugging=(boolean)
39
+ @sub_protocols.each do |klass|
40
+ klass.debugging = boolean
41
+ end
42
+ @@debugging = boolean
43
+ end
44
+ def self.debug!
45
+ @sub_protocols.each do |klass|
46
+ klass.debug!
47
+ end
48
+ self.debugging = true
49
+ end
50
+ def self.debug(&block)
51
+ sub_protocol_originals = @sub_protocols.inject({}) do |sub_protocol_originals, klass|
52
+ sub_protocol_originals.merge!(klass => klass.debugging?)
53
+ end
54
+ original = self.debugging?
55
+ debug!
56
+ yield
57
+ ensure
58
+ @sub_protocols.each do |klass|
59
+ klass.debugging = sub_protocol_originals[klass]
60
+ end
61
+ self.debugging = original
62
+ end
63
+ self.debugging = false
64
+
65
+ def self.http_client
66
+ Faraday.new(headers: {user_agent: "Oidc (#{VERSION})"}) do |faraday|
67
+ faraday.request :url_encoded
68
+ faraday.request :json
69
+ faraday.response :json
70
+ faraday.adapter Faraday.default_adapter
71
+ http_config&.call(faraday)
72
+ faraday.response :logger, Oidc.logger, {bodies: true} if debugging?
73
+ end
74
+ end
75
+ def self.http_config(&block)
76
+ @sub_protocols.each do |klass|
77
+ klass.http_config(&block) unless klass.http_config
78
+ end
79
+ @@http_config ||= block
80
+ end
81
+
82
+ def self.validate_discovery_issuer=(boolean)
83
+ @@validate_discovery_issuer = boolean
84
+ end
85
+
86
+ def self.validate_discovery_issuer
87
+ @@validate_discovery_issuer
88
+ end
89
+
90
+ self.validate_discovery_issuer = true
91
+ end
92
+
93
+ require 'oidc/exception'
94
+ require 'oidc/client'
95
+ require 'oidc/access_token'
96
+ require 'oidc/jwtnizable'
97
+ require 'oidc/connect_object'
98
+ require 'oidc/discovery'
@@ -0,0 +1,34 @@
1
+ module Rack
2
+ module OAuth2
3
+ module Server
4
+ class Authorize
5
+ module ErrorWithConnectExt
6
+ DEFAULT_DESCRIPTION = {
7
+ invalid_redirect_uri: 'The redirect_uri in the request does not match any of pre-registered redirect_uris.',
8
+ interaction_required: 'End-User interaction required.',
9
+ login_required: 'End-User authentication required.',
10
+ session_selection_required: 'The End-User is required to select a session at the Authorization Server.',
11
+ consent_required: 'End-User consent required.',
12
+ invalid_request_uri: 'The request_uri in the request returns an error or invalid data.',
13
+ invalid_openid_request_object: 'The request parameter contains an invalid OpenID Request Object.'
14
+ }
15
+
16
+ def self.included(klass)
17
+ DEFAULT_DESCRIPTION.each do |error, default_description|
18
+ # NOTE:
19
+ # Connect Message spec doesn't say anything about HTTP status code for each error code.
20
+ # It probably means "use 400".
21
+ error_method = :bad_request!
22
+ klass.class_eval <<-ERROR
23
+ def #{error}!(description = "#{default_description}", options = {})
24
+ #{error_method} :#{error}, description, options
25
+ end
26
+ ERROR
27
+ end
28
+ end
29
+ end
30
+ Request.send :include, ErrorWithConnectExt
31
+ end
32
+ end
33
+ end
34
+ end