masks 0.9.2 → 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,33 @@
1
+ module Masks
2
+ module Client
3
+ class Enrollment
4
+ attr_reader :initial_access_token, :scope, :expires_in, :obtained_at
5
+
6
+ def self.issued(body)
7
+ if body["initial_access_token"].to_s.empty?
8
+ raise Rejected.new(
9
+ body["error"] || "invalid_enrollment_response",
10
+ body["error_description"] || "the enrollment endpoint answered without an initial_access_token"
11
+ )
12
+ end
13
+
14
+ new(body)
15
+ end
16
+
17
+ def initialize(body)
18
+ @initial_access_token = body["initial_access_token"]
19
+ @scope = body["scope"].to_s
20
+ @expires_in = body["expires_in"].to_i
21
+ @obtained_at = Time.now.to_i
22
+ end
23
+
24
+ def expires_at
25
+ obtained_at + expires_in
26
+ end
27
+
28
+ def scopes
29
+ scope.split
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,6 +4,12 @@ module Masks
4
4
 
5
5
  class Unreachable < Error; end
6
6
 
7
+ class Foreign < Error; end
8
+
9
+ class Insecure < Error; end
10
+
11
+ class ReplayUnavailable < Error; end
12
+
7
13
  class Rejected < Error
8
14
  attr_reader :code, :description, :status
9
15
 
@@ -3,6 +3,7 @@ module Masks
3
3
  module HTTP
4
4
  OPEN_TIMEOUT = 5
5
5
  READ_TIMEOUT = 10
6
+ READ_LIMIT = 1 << 20
6
7
 
7
8
  module_function
8
9
 
@@ -11,12 +12,22 @@ module Masks
11
12
  end
12
13
 
13
14
  def post_form(url, form, headers = {})
14
- uri = URI.parse(url.to_s)
15
- request = Net::HTTP::Post.new(uri, default_headers.merge(headers))
15
+ encoded(Net::HTTP::Post, url, form, headers)
16
+ end
17
+
18
+ def delete_form(url, form, headers = {})
19
+ encoded(Net::HTTP::Delete, url, form, headers)
20
+ end
21
+
22
+ def encoded(verb, url, form, headers)
23
+ request(form_request(verb, url, form, headers))
24
+ end
25
+
26
+ def form_request(verb, url, form, headers)
27
+ request = verb.new(URI.parse(url.to_s), default_headers.merge(headers))
16
28
  request.body = URI.encode_www_form(form)
17
29
  request["Content-Type"] = "application/x-www-form-urlencoded"
18
-
19
- self.request(request)
30
+ request
20
31
  end
21
32
 
22
33
  def post_json(url, body, headers = {})
@@ -32,7 +43,10 @@ module Masks
32
43
  end
33
44
 
34
45
  def fetch(url, headers = {})
35
- uri = URI.parse(url.to_s)
46
+ uri = checked(URI.parse(url.to_s))
47
+ embedded = Masks::Client.embedded_for(uri)
48
+
49
+ return embedded.deliver(Net::HTTP::Get.new(uri, headers)) if embedded
36
50
 
37
51
  Net::HTTP.start(
38
52
  uri.hostname, uri.port,
@@ -58,26 +72,62 @@ module Masks
58
72
  end
59
73
 
60
74
  def request(request)
61
- uri = request.uri
75
+ parse(deliver(request))
76
+ end
77
+
78
+ def deliver(request, limit: READ_LIMIT)
79
+ uri = checked(request.uri)
80
+ embedded = Masks::Client.embedded_for(uri)
62
81
 
63
- response = Net::HTTP.start(
82
+ return embedded.deliver(request, limit: limit) if embedded
83
+
84
+ Net::HTTP.start(
64
85
  uri.hostname, uri.port,
65
86
  use_ssl: uri.scheme == "https",
66
87
  open_timeout: OPEN_TIMEOUT,
67
88
  read_timeout: READ_TIMEOUT
68
- ) { |http| http.request(request) }
69
-
70
- parse(response)
89
+ ) { |http| limit ? limited(http, request, limit) : http.request(request) }
71
90
  rescue SystemCallError, SocketError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError => e
72
91
  raise Unreachable, "#{uri.host} is unreachable (#{e.class})"
73
92
  end
74
93
 
75
- def parse(response)
76
- body = begin
77
- JSON.parse(response.body.to_s)
78
- rescue JSON::ParserError
79
- {}
94
+ def checked(uri)
95
+ return uri if uri.is_a?(URI::HTTPS) && !uri.host.to_s.empty?
96
+ return uri if uri.is_a?(URI::HTTP) && !uri.host.to_s.empty? && (Masks::Client.plain_http? || loopback?(uri.hostname))
97
+
98
+ if uri.is_a?(URI::HTTP)
99
+ raise Insecure, "#{Nonces.origin(uri)} is plain http, which masks allows only for loopback hosts " \
100
+ "unless Masks::Client.plain_http is set"
101
+ end
102
+
103
+ raise Error, "#{uri} is not an http or https URL"
104
+ end
105
+
106
+ def loopback?(host)
107
+ name = host.to_s.downcase
108
+
109
+ return true if name == "localhost" || name.end_with?(".localhost")
110
+
111
+ IPAddr.new(name).loopback?
112
+ rescue IPAddr::Error
113
+ false
114
+ end
115
+
116
+ def limited(http, request, limit)
117
+ http.request(request) do |response|
118
+ body = +""
119
+
120
+ response.read_body do |chunk|
121
+ body << chunk
122
+ raise Error, "#{request.uri.host} answered with more than #{limit} bytes" if body.bytesize > limit
123
+ end
124
+
125
+ response.body = body
80
126
  end
127
+ end
128
+
129
+ def parse(response)
130
+ body = decoded(response)
81
131
 
82
132
  unless response.is_a?(Net::HTTPSuccess)
83
133
  refusal = Unregistered.raised_by?(body) ? Unregistered : Rejected
@@ -91,6 +141,30 @@ module Masks
91
141
 
92
142
  body
93
143
  end
144
+
145
+ def decoded(response)
146
+ raw = response.body.to_s
147
+ return {} if raw.strip.empty?
148
+
149
+ held = JSON.parse(raw)
150
+
151
+ return held if held.is_a?(Hash)
152
+ return {} unless response.is_a?(Net::HTTPSuccess)
153
+
154
+ raise Rejected.new("invalid_response", "#{origin(response)} answered with JSON that is not an object",
155
+ status: response.code.to_i)
156
+ rescue JSON::ParserError
157
+ return {} unless response.is_a?(Net::HTTPSuccess)
158
+
159
+ raise Rejected.new("invalid_response", "#{origin(response)} answered with something other than JSON",
160
+ status: response.code.to_i)
161
+ end
162
+
163
+ def origin(response)
164
+ uri = response.uri
165
+
166
+ uri ? Nonces.origin(uri) : "the server"
167
+ end
94
168
  end
95
169
  end
96
170
  end
@@ -14,13 +14,14 @@ module Masks
14
14
  Masks::Client.registry[issuer, ttl: ttl]
15
15
  end
16
16
 
17
- attr_reader :url
17
+ attr_reader :url, :nonces
18
18
 
19
19
  def initialize(url, ttl: TTL)
20
20
  @url = self.class.normalize(url)
21
21
  @ttl = ttl
22
22
  @cache = {}
23
23
  @lock = Mutex.new
24
+ @nonces = Nonces.new
24
25
  end
25
26
 
26
27
  def discovery
@@ -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