oidc 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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