masks 0.10.0 → 0.11.0

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.
@@ -0,0 +1,100 @@
1
+ module Masks
2
+ module Client
3
+ class Key
4
+ CURVE = "prime256v1".freeze
5
+ ALGORITHM = "ES256".freeze
6
+ PUBLIC = %w[kty crv x y].freeze
7
+
8
+ def self.generate
9
+ new(OpenSSL::PKey::EC.generate(CURVE))
10
+ end
11
+
12
+ def self.parse(data)
13
+ held = data.is_a?(String) && !data.lstrip.start_with?("{") ? OpenSSL::PKey.read(data) : imported(data)
14
+
15
+ new(held)
16
+ rescue OpenSSL::PKey::PKeyError, JSON::ParserError, JWT::JWKError, ArgumentError
17
+ raise ArgumentError, "a key is a private P-256 key, as a PEM or a JWK"
18
+ end
19
+
20
+ def self.from(value)
21
+ case value
22
+ when self then value
23
+ when OpenSSL::PKey::PKey then new(value)
24
+ else parse(value)
25
+ end
26
+ end
27
+
28
+ def self.imported(data)
29
+ held = data.is_a?(String) ? JSON.parse(data) : data.to_h
30
+
31
+ JWT::JWK.import(held.transform_keys(&:to_sym)).signing_key
32
+ end
33
+
34
+ def self.htu(url)
35
+ uri = URI.parse(url.to_s)
36
+ uri.query = nil
37
+ uri.fragment = nil
38
+
39
+ uri.to_s
40
+ end
41
+
42
+ attr_reader :private_key, :thumbprint
43
+
44
+ def initialize(private_key)
45
+ unless private_key.is_a?(OpenSSL::PKey::EC) && private_key.private? && private_key.group.curve_name == CURVE
46
+ raise ArgumentError, "a key is a private P-256 key"
47
+ end
48
+
49
+ @private_key = private_key
50
+ @public = JWT::JWK.new(private_key).export.transform_keys(&:to_s).slice(*PUBLIC).freeze
51
+ @thumbprint = Proof.thumbprint(@public)
52
+ end
53
+
54
+ alias_method :kid, :thumbprint
55
+
56
+ def public_jwk
57
+ @public.merge("kid" => thumbprint, "alg" => ALGORITHM, "use" => "sig")
58
+ end
59
+
60
+ def jwks
61
+ { "keys" => [ public_jwk ] }
62
+ end
63
+
64
+ def to_h
65
+ JWT::JWK.new(private_key).export(include_private: true).transform_keys(&:to_s)
66
+ .merge("kid" => thumbprint, "alg" => ALGORITHM, "use" => "sig")
67
+ end
68
+
69
+ def to_json(*)
70
+ JSON.generate(to_h)
71
+ end
72
+
73
+ def to_pem
74
+ private_key.private_to_pem
75
+ end
76
+
77
+ def inspect
78
+ "#<#{self.class.name} #{thumbprint}>"
79
+ end
80
+
81
+ def sign(claims, header = {})
82
+ JWT.encode(claims, private_key, ALGORITHM, header)
83
+ end
84
+
85
+ def proof(method, url, access_token: nil, nonce: nil)
86
+ claims = {
87
+ "jti" => SecureRandom.urlsafe_base64(24),
88
+ "htm" => method.to_s.upcase,
89
+ "htu" => self.class.htu(url),
90
+ "iat" => Time.now.to_i
91
+ }
92
+
93
+ claims["ath"] = Proof.digest(access_token) unless access_token.to_s.empty?
94
+ claims["nonce"] = nonce unless nonce.to_s.empty?
95
+
96
+ sign(claims, { typ: Proof::TYPE, jwk: @public })
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,31 @@
1
+ module Masks
2
+ module Client
3
+ class Nonces
4
+ HEADER = "DPoP-Nonce".freeze
5
+
6
+ def self.origin(url)
7
+ uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
8
+ port = ":#{uri.port}" unless uri.port.nil? || uri.port == uri.default_port
9
+
10
+ "#{uri.scheme.to_s.downcase}://#{uri.host.to_s.downcase}#{port}"
11
+ end
12
+
13
+ def initialize
14
+ @held = {}
15
+ @lock = Mutex.new
16
+ end
17
+
18
+ def [](url)
19
+ @lock.synchronize { @held[self.class.origin(url)] }
20
+ end
21
+
22
+ def learn(url, response)
23
+ wanted = response[HEADER].to_s.strip
24
+
25
+ return nil if wanted.empty?
26
+
27
+ @lock.synchronize { @held[self.class.origin(url)] = wanted }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -139,11 +139,17 @@ module Masks
139
139
  jti = claims["jti"].to_s
140
140
 
141
141
  raise Invalid, "a proof must carry a jti" if jti.empty?
142
- return if @replay.nil? || @replay.first?("#{thumbprint}:#{jti}", expires_in: MEMORY)
142
+ return if @replay.nil? || first?("#{thumbprint}:#{jti}")
143
143
 
144
144
  raise Invalid, "that proof has already been used"
145
145
  end
146
146
 
147
+ def first?(key)
148
+ @replay.first?(key, expires_in: MEMORY)
149
+ rescue StandardError => e
150
+ raise ReplayUnavailable, "the replay store could not say whether that proof is new (#{e.class})"
151
+ end
152
+
147
153
  def same?(held, expected)
148
154
  !held.to_s.empty? && held.bytesize == expected.to_s.bytesize &&
149
155
  OpenSSL.fixed_length_secure_compare(held, expected.to_s)
@@ -25,6 +25,8 @@ module Masks
25
25
  return passthrough(env, e) if @optional && e.is_a?(Unauthenticated)
26
26
 
27
27
  refuse(resource(env), e)
28
+ rescue ReplayUnavailable
29
+ unavailable
28
30
  end
29
31
 
30
32
  private
@@ -56,6 +58,12 @@ module Masks
56
58
  @resource.respond_to?(:call) ? @resource.call(env) : @resource
57
59
  end
58
60
 
61
+ def unavailable
62
+ body = JSON.generate({ "error" => "temporarily_unavailable", "error_description" => "the token could not be checked" })
63
+
64
+ [ 503, { "content-type" => "application/json", "cache-control" => "no-store" }, [ body ] ]
65
+ end
66
+
59
67
  def refuse(resource, error)
60
68
  body = JSON.generate(
61
69
  { "error" => error.code, "error_description" => error.description }.compact
@@ -6,31 +6,41 @@ module Masks
6
6
  def self.create(issuer, token: nil, **attributes)
7
7
  issuer = Issuer.resolve(issuer)
8
8
  headers = token ? { "Authorization" => "Bearer #{token}" } : {}
9
- body = HTTP.post_json(
10
- issuer.endpoint("registration_endpoint"), stringify(attributes), headers
11
- )
9
+ body = HTTP.post_json(endpoint(issuer, token), stringify(attributes), headers)
12
10
 
13
11
  new(issuer, body)
14
12
  end
15
13
 
14
+ def self.endpoint(issuer, token)
15
+ return issuer.endpoint("registration_endpoint") if token.nil? || issuer.discovery["registration_endpoint"]
16
+
17
+ "#{issuer.url}/register"
18
+ end
19
+
16
20
  def self.stringify(attributes)
17
21
  {
18
22
  "client_name" => attributes[:name] || attributes[:client_name],
19
23
  "redirect_uris" => Array(attributes[:redirect_uris]),
20
- "grant_types" => fallback(attributes[:grant_types], %w[authorization_code]),
21
- "response_types" => fallback(attributes[:response_types], %w[code]),
24
+ "grant_types" => Array(attributes[:grant_types]),
25
+ "response_types" => Array(attributes[:response_types]),
22
26
  "scope" => Array(attributes[:scope]).join(" "),
23
27
  "token_endpoint_auth_method" => attributes[:token_endpoint_auth_method],
24
28
  "application_type" => attributes[:application_type],
25
29
  "client_uri" => attributes[:client_uri],
26
30
  "logo_uri" => attributes[:logo_uri],
27
- "backchannel_logout_uri" => attributes[:backchannel_logout_uri]
31
+ "backchannel_logout_uri" => attributes[:backchannel_logout_uri],
32
+ "jwks" => keyset(attributes[:jwks]),
33
+ "dpop_bound_access_tokens" => attributes[:dpop_bound_access_tokens]
28
34
  }.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
29
35
  end
30
36
 
31
- def self.fallback(value, default)
32
- list = Array(value)
33
- list.empty? ? default : list
37
+ def self.keyset(value)
38
+ case value
39
+ when nil then nil
40
+ when Key then value.jwks
41
+ when Array then { "keys" => value.map { |key| key.is_a?(Key) ? key.public_jwk : key } }
42
+ else value
43
+ end
34
44
  end
35
45
 
36
46
  def self.held(issuer, credentials)
@@ -62,18 +72,18 @@ module Masks
62
72
  GONE = (REFUSED + [ 404 ]).freeze
63
73
 
64
74
  def read
65
- still_ours(REFUSED) { HTTP.get(uri, authorization) }
75
+ still_ours(REFUSED) { HTTP.get(owned, authorization) }
66
76
  end
67
77
 
68
78
  def update(**attributes)
69
79
  @metadata = metadata.merge(
70
- still_ours(REFUSED) { HTTP.put_json(uri, self.class.stringify(attributes), authorization) }
80
+ still_ours(REFUSED) { HTTP.put_json(owned, self.class.stringify(attributes), authorization) }
71
81
  )
72
82
  self
73
83
  end
74
84
 
75
85
  def delete
76
- still_ours(GONE) { HTTP.delete(uri, authorization) }
86
+ still_ours(GONE) { HTTP.delete(owned, authorization) }
77
87
  true
78
88
  end
79
89
 
@@ -88,18 +98,25 @@ module Masks
88
98
  { "Authorization" => "Bearer #{access_token}" }
89
99
  end
90
100
 
91
- def session(redirect_uri:, scope: Session::DEFAULT_SCOPE)
101
+ def session(redirect_uri: nil, scope: Session::DEFAULT_SCOPE, **options)
92
102
  Session.new(
93
103
  issuer: issuer,
94
104
  client_id: client_id,
95
- client_secret: client_secret,
105
+ client_secret: (client_secret unless options[:private_key]),
96
106
  redirect_uri: redirect_uri,
97
- scope: scope
107
+ scope: scope,
108
+ **options
98
109
  )
99
110
  end
100
111
 
101
112
  private
102
113
 
114
+ def owned
115
+ return uri if Nonces.origin(uri) == Nonces.origin(issuer.url)
116
+
117
+ raise Foreign, "the registration names #{Nonces.origin(uri)}, which is not #{issuer.url}"
118
+ end
119
+
103
120
  def still_ours(statuses)
104
121
  yield
105
122
  rescue Unregistered
@@ -13,17 +13,21 @@ module Masks
13
13
  ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".freeze
14
14
  ASSERTION_LIFETIME = 60
15
15
 
16
- attr_reader :issuer, :client_id, :client_secret, :redirect_uri, :scope, :private_key, :key_id
16
+ NO_NONCE = "use_dpop_nonce".freeze
17
+
18
+ attr_reader :issuer, :client_id, :client_secret, :redirect_uri, :scope, :private_key, :key_id, :dpop
17
19
 
18
20
  def initialize(issuer:, client_id:, redirect_uri: nil, client_secret: nil, private_key: nil, key_id: nil,
19
- scope: DEFAULT_SCOPE)
21
+ dpop: nil, scope: DEFAULT_SCOPE)
20
22
  raise ArgumentError, "a client authenticates with a secret or a private key, not both" if client_secret && private_key
21
23
 
22
24
  @issuer = Issuer.resolve(issuer)
23
25
  @client_id = client_id
24
26
  @client_secret = client_secret
25
- @private_key = private_key.is_a?(String) ? OpenSSL::PKey.read(private_key) : private_key
26
- @key_id = key_id
27
+ @private_key = private_key.is_a?(Key) ? private_key.private_key : private_key
28
+ @private_key = OpenSSL::PKey.read(@private_key) if @private_key.is_a?(String)
29
+ @key_id = key_id || (private_key.thumbprint if private_key.is_a?(Key))
30
+ @dpop = dpop && Key.from(dpop)
27
31
  @redirect_uri = redirect_uri
28
32
  @scope = Array(scope)
29
33
  end
@@ -71,7 +75,7 @@ module Masks
71
75
 
72
76
  Array(resource).each { |value| form << [ "resource", value ] }
73
77
 
74
- Tokens.granted(post("token_endpoint", form))
78
+ token(form)
75
79
  end
76
80
 
77
81
  def client_credentials(scope: nil, resource: nil)
@@ -80,7 +84,23 @@ module Masks
80
84
  form << [ "scope", Array(scope).join(" ") ] if scope
81
85
  Array(resource).each { |value| form << [ "resource", value ] }
82
86
 
83
- Tokens.granted(post("token_endpoint", form))
87
+ token(form)
88
+ end
89
+
90
+ def enroll(scope:, name: nil, expires_in: nil)
91
+ form = [ [ "client_id", client_id ], [ "scope", Array(scope).join(" ") ] ]
92
+
93
+ form << [ "client_name", name ] if name
94
+ form << [ "expires_in", expires_in.to_i ] if expires_in
95
+
96
+ Enrollment.issued(post("enrollment_endpoint", form))
97
+ end
98
+
99
+ def unenroll(machine_id)
100
+ url = "#{issuer.endpoint('enrollment_endpoint')}/#{URI.encode_www_form_component(machine_id)}"
101
+
102
+ HTTP.delete_form(url, [ [ "client_id", client_id ] ] + assertion, authorization)
103
+ true
84
104
  end
85
105
 
86
106
  def refresh(refresh_token, resource: nil, scope: nil)
@@ -93,7 +113,7 @@ module Masks
93
113
  form << [ "scope", Array(scope).join(" ") ] if scope
94
114
  Array(resource).each { |value| form << [ "resource", value ] }
95
115
 
96
- Tokens.granted(post("token_endpoint", form))
116
+ token(form)
97
117
  end
98
118
 
99
119
  def exchange(subject_token, scope: nil, resource: nil, lifetime: nil, requested_token_type: nil, audience: nil,
@@ -114,7 +134,46 @@ module Masks
114
134
  form << [ "requested_lifetime", lifetime.to_i ] if lifetime
115
135
  Array(resource).each { |value| form << [ "resource", value ] }
116
136
 
117
- Tokens.granted(post("token_endpoint", form))
137
+ token(form)
138
+ end
139
+
140
+ def device_authorization(scope: nil, resource: nil)
141
+ form = [ [ "client_id", client_id ] ]
142
+
143
+ form << [ "scope", Array(scope).join(" ") ] if scope
144
+ Array(resource).each { |value| form << [ "resource", value ] }
145
+
146
+ Device.issued(post("device_authorization_endpoint", form))
147
+ end
148
+
149
+ def await_device(device, pause: Device::INTERVAL)
150
+ form = [ [ "grant_type", Device::GRANT_TYPE ], [ "device_code", device.device_code ], [ "client_id", client_id ] ]
151
+ interval = device.interval.positive? ? device.interval : pause
152
+
153
+ loop do
154
+ raise Rejected.new("expired_token", "the code expired before anyone approved it") if device.expired?
155
+
156
+ sleep(interval)
157
+
158
+ begin
159
+ return token(form)
160
+ rescue Rejected => e
161
+ case e.code
162
+ when "authorization_pending" then next
163
+ when "slow_down" then interval += pause
164
+ else raise
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def transport(*origins, tokens: nil, scope: nil, resource: nil, &keep)
171
+ Transport.new(*origins, tokens: tokens, key: dpop) do |held|
172
+ renewed = tokens ? renew(held, scope: scope, resource: resource) : client_credentials(scope: scope, resource: resource)
173
+
174
+ keep&.call(renewed)
175
+ renewed
176
+ end
118
177
  end
119
178
 
120
179
  def revoke(token, hint: nil)
@@ -147,8 +206,13 @@ module Masks
147
206
  )
148
207
  end
149
208
 
150
- def userinfo(access_token)
151
- HTTP.get(issuer.endpoint("userinfo_endpoint"), "Authorization" => "Bearer #{access_token}")
209
+ def userinfo(tokens)
210
+ url = issuer.endpoint("userinfo_endpoint")
211
+
212
+ return HTTP.get(url, "Authorization" => "Bearer #{tokens}") unless tokens.is_a?(Tokens)
213
+ return HTTP.get(url, "Authorization" => "Bearer #{tokens.access_token}") unless tokens.bound?
214
+
215
+ HTTP.parse(Transport.new(url, tokens: tokens, key: dpop).request("GET", url, limit: HTTP::READ_LIMIT))
152
216
  end
153
217
 
154
218
  def identity(tokens)
@@ -162,19 +226,64 @@ module Masks
162
226
  def profile(tokens)
163
227
  return {} if tokens.access_token.nil?
164
228
 
165
- userinfo(tokens.access_token)
229
+ userinfo(tokens)
166
230
  rescue Masks::Client::Error
167
231
  {}
168
232
  end
169
233
 
170
234
  private
171
235
 
236
+ def renew(held, scope:, resource:)
237
+ if held&.refresh_token.to_s.empty?
238
+ raise Rejected.new("invalid_grant", "there is no refresh token to renew these tokens with")
239
+ end
240
+
241
+ renewed = refresh(held.refresh_token, scope: scope, resource: resource)
242
+
243
+ return renewed if renewed.refresh_token
244
+
245
+ Tokens.from_h(renewed.to_h.merge("refresh_token" => held.refresh_token))
246
+ end
247
+
248
+ def token(form)
249
+ tokens = Tokens.granted(dpop ? proven(issuer.endpoint("token_endpoint"), form) : post("token_endpoint", form))
250
+
251
+ if dpop && !tokens.bound?
252
+ raise Rejected.new("invalid_dpop_proof", "the token endpoint answered with a token that is not held to this key")
253
+ end
254
+
255
+ tokens
256
+ end
257
+
172
258
  def post(endpoint, form)
173
259
  url = issuer.endpoint(endpoint)
174
260
 
175
261
  HTTP.post_form(url, form + assertion, authorization)
176
262
  end
177
263
 
264
+ def proven(url, form)
265
+ nonce = issuer.nonces[url]
266
+ response = HTTP.deliver(proven_request(url, form, nonce))
267
+ wanted = issuer.nonces.learn(url, response)
268
+
269
+ begin
270
+ HTTP.parse(response)
271
+ rescue Rejected => e
272
+ raise unless e.code == NO_NONCE && wanted && wanted != nonce
273
+
274
+ response = HTTP.deliver(proven_request(url, form, wanted))
275
+ issuer.nonces.learn(url, response)
276
+
277
+ HTTP.parse(response)
278
+ end
279
+ end
280
+
281
+ def proven_request(url, form, nonce)
282
+ headers = authorization.merge("DPoP" => dpop.proof("POST", url, nonce: nonce))
283
+
284
+ HTTP.form_request(Net::HTTP::Post, url, form + assertion, headers)
285
+ end
286
+
178
287
  def assertion
179
288
  return [] if private_key.nil?
180
289
 
@@ -44,6 +44,10 @@ module Masks
44
44
  scope.split(/\s+/).reject(&:empty?)
45
45
  end
46
46
 
47
+ def bound?
48
+ token_type.to_s.casecmp?(Proof::SCHEME)
49
+ end
50
+
47
51
  def authorization
48
52
  "#{token_type} #{access_token}"
49
53
  end
@@ -0,0 +1,114 @@
1
+ module Masks
2
+ module Client
3
+ class Transport
4
+ VERBS = {
5
+ "GET" => Net::HTTP::Get, "HEAD" => Net::HTTP::Head, "POST" => Net::HTTP::Post, "PUT" => Net::HTTP::Put,
6
+ "PATCH" => Net::HTTP::Patch, "DELETE" => Net::HTTP::Delete
7
+ }.freeze
8
+
9
+ attr_reader :origins, :key
10
+
11
+ def initialize(*origins, tokens: nil, key: nil, &renew)
12
+ raise ArgumentError, "a transport needs the origins it may send its tokens to" if origins.empty?
13
+ raise ArgumentError, "a transport needs tokens or a way to fetch them" if tokens.nil? && renew.nil?
14
+
15
+ @origins = origins.flatten.map { |origin| Nonces.origin(HTTP.checked(URI.parse(origin.to_s))) }.uniq.freeze
16
+ @tokens = tokens
17
+ @key = key && Key.from(key)
18
+ @renew = renew
19
+ @lock = Mutex.new
20
+ @nonces = Nonces.new
21
+ end
22
+
23
+ def tokens
24
+ @lock.synchronize do
25
+ @tokens = @renew.call(@tokens) if @renew && (@tokens.nil? || @tokens.expired?)
26
+ @tokens
27
+ end
28
+ end
29
+
30
+ def get(url, headers: {})
31
+ request("GET", url, headers: headers)
32
+ end
33
+
34
+ def head(url, headers: {})
35
+ request("HEAD", url, headers: headers)
36
+ end
37
+
38
+ def delete(url, headers: {})
39
+ request("DELETE", url, headers: headers)
40
+ end
41
+
42
+ def post(url, body: nil, headers: {})
43
+ request("POST", url, body: body, headers: headers)
44
+ end
45
+
46
+ def put(url, body: nil, headers: {})
47
+ request("PUT", url, body: body, headers: headers)
48
+ end
49
+
50
+ def patch(url, body: nil, headers: {})
51
+ request("PATCH", url, body: body, headers: headers)
52
+ end
53
+
54
+ def request(method, url, body: nil, headers: {}, limit: nil)
55
+ uri = URI.parse(url.to_s)
56
+ verb = VERBS.fetch(method.to_s.upcase) { raise ArgumentError, "#{method} is not a method a transport sends" }
57
+
58
+ unless origins.include?(Nonces.origin(uri))
59
+ raise Foreign, "this transport holds tokens only for #{origins.join(', ')}"
60
+ end
61
+
62
+ held = tokens
63
+ nonce = @nonces[uri] if held.bound?
64
+ response = HTTP.deliver(signed(verb, uri, body, headers, held, nonce), limit: limit)
65
+
66
+ return response unless held.bound?
67
+
68
+ wanted = @nonces.learn(uri, response)
69
+
70
+ return response unless wanted && wanted != nonce && nonce_asked?(response)
71
+
72
+ HTTP.deliver(signed(verb, uri, body, headers, held, wanted), limit: limit)
73
+ end
74
+
75
+ def inspect
76
+ "#<#{self.class.name} #{origins.join(', ')}>"
77
+ end
78
+
79
+ private
80
+
81
+ def signed(verb, uri, body, headers, held, nonce)
82
+ request = verb.new(uri, HTTP.default_headers.merge(headers))
83
+
84
+ if held.bound?
85
+ raise Error, "a DPoP token needs the key it is held to" if key.nil?
86
+
87
+ request["Authorization"] = "#{Proof::SCHEME} #{held.access_token}"
88
+ request["DPoP"] = key.proof(request.method, uri, access_token: held.access_token, nonce: nonce)
89
+ else
90
+ request["Authorization"] = "Bearer #{held.access_token}"
91
+ end
92
+
93
+ written(request, body)
94
+ end
95
+
96
+ def written(request, body)
97
+ return request if body.nil?
98
+
99
+ if body.is_a?(String)
100
+ request.body = body
101
+ else
102
+ request.body = JSON.generate(body)
103
+ request["Content-Type"] ||= "application/json"
104
+ end
105
+
106
+ request
107
+ end
108
+
109
+ def nonce_asked?(response)
110
+ response.code.to_i == 401 && response["WWW-Authenticate"].to_s.include?("use_dpop_nonce")
111
+ end
112
+ end
113
+ end
114
+ end
data/lib/masks/client.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "base64"
2
+ require "ipaddr"
2
3
  require "json"
3
4
  require "jwt"
4
5
  require "net/http"
@@ -9,14 +10,20 @@ require "uri"
9
10
  require_relative "version"
10
11
  require_relative "client/errors"
11
12
  require_relative "client/http"
13
+ require_relative "client/embedded"
12
14
  require_relative "client/pkce"
13
15
  require_relative "client/stores"
14
16
  require_relative "client/tracker"
17
+ require_relative "client/nonces"
18
+ require_relative "client/key"
15
19
  require_relative "client/issuer"
16
20
  require_relative "client/registry"
17
21
  require_relative "client/tokens"
18
22
  require_relative "client/claims"
19
23
  require_relative "client/introspection"
24
+ require_relative "client/enrollment"
25
+ require_relative "client/device"
26
+ require_relative "client/transport"
20
27
  require_relative "client/session"
21
28
  require_relative "client/verifier"
22
29
  require_relative "client/proof"
@@ -44,6 +51,20 @@ module Masks
44
51
  # state of its own beyond the discovery cache in ::registry.
45
52
  module Client
46
53
  class << self
54
+ attr_writer :plain_http
55
+
56
+ def plain_http?
57
+ @plain_http == true
58
+ end
59
+
60
+ def embed(app, &serves)
61
+ @embedded = app && Embedded.new(app, &serves)
62
+ end
63
+
64
+ def embedded_for(uri)
65
+ @embedded if @embedded&.serves?(uri)
66
+ end
67
+
47
68
  # The process-wide cache of resolved issuers, so discovery is fetched
48
69
  # once rather than per request.
49
70
  #