masks 0.10.0 → 0.12.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/README.md +28 -341
- data/app/controllers/masks/rails/handshakes_controller.rb +2 -2
- data/app/controllers/masks/rails/logouts_controller.rb +11 -8
- data/app/controllers/masks/rails/sessions_controller.rb +48 -6
- data/config/routes.rb +1 -0
- data/lib/masks/client/device.rb +47 -0
- data/lib/masks/client/embedded.rb +125 -0
- data/lib/masks/client/enrollment.rb +33 -0
- data/lib/masks/client/errors.rb +6 -0
- data/lib/masks/client/handshake.rb +12 -4
- data/lib/masks/client/http.rb +87 -24
- data/lib/masks/client/issuer.rb +2 -1
- data/lib/masks/client/key.rb +100 -0
- data/lib/masks/client/logout.rb +5 -1
- data/lib/masks/client/nonces.rb +31 -0
- data/lib/masks/client/proof.rb +7 -1
- data/lib/masks/client/rack.rb +8 -0
- data/lib/masks/client/registration.rb +32 -15
- data/lib/masks/client/resource.rb +1 -1
- data/lib/masks/client/session.rb +120 -11
- data/lib/masks/client/tokens.rb +4 -0
- data/lib/masks/client/transport.rb +114 -0
- data/lib/masks/client/verifier.rb +10 -2
- data/lib/masks/client.rb +21 -4
- data/lib/masks/rails/authentication.rb +115 -35
- data/lib/masks/rails/cache.rb +173 -0
- data/lib/masks/rails/configuration.rb +27 -12
- data/lib/masks/rails/protected_resource.rb +13 -0
- data/lib/masks/rails/renewal.rb +54 -0
- data/lib/masks/rails/session_cookies.rb +145 -0
- data/lib/masks/rails.rb +5 -4
- data/lib/masks/version.rb +1 -1
- data/lib/masks.rb +0 -1
- metadata +10 -1
|
@@ -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" =>
|
|
21
|
-
"response_types" =>
|
|
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.
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
@@ -57,7 +57,7 @@ module Masks
|
|
|
57
57
|
|
|
58
58
|
parameters = [
|
|
59
59
|
[ "error", error.code ],
|
|
60
|
-
[ "error_description", error.description ],
|
|
60
|
+
[ "error_description", (error.description if error.code) ],
|
|
61
61
|
[ "scope", error.scope || (scopes.join(" ") if scopes.any?) ],
|
|
62
62
|
[ "resource_metadata", metadata_url ]
|
|
63
63
|
]
|
data/lib/masks/client/session.rb
CHANGED
|
@@ -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
|
-
|
|
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?(
|
|
26
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
151
|
-
|
|
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
|
|
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
|
|
data/lib/masks/client/tokens.rb
CHANGED
|
@@ -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
|
|
@@ -28,8 +28,16 @@ module Masks
|
|
|
28
28
|
typed!(header["typ"], Array(typ))
|
|
29
29
|
|
|
30
30
|
claims
|
|
31
|
-
rescue JWT::
|
|
32
|
-
raise InvalidToken,
|
|
31
|
+
rescue JWT::ExpiredSignature
|
|
32
|
+
raise InvalidToken, "that token has expired"
|
|
33
|
+
rescue JWT::InvalidIssuerError
|
|
34
|
+
raise InvalidToken, "that token was issued by another server"
|
|
35
|
+
rescue JWT::InvalidAudError
|
|
36
|
+
raise InvalidToken, "that token was issued for another app"
|
|
37
|
+
rescue JWT::MissingRequiredClaim
|
|
38
|
+
raise InvalidToken, "that token is missing a claim it needs"
|
|
39
|
+
rescue JWT::DecodeError
|
|
40
|
+
raise InvalidToken, "that token was not signed by #{issuer.url}"
|
|
33
41
|
end
|
|
34
42
|
|
|
35
43
|
private
|
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"
|
|
@@ -39,11 +46,22 @@ module Masks
|
|
|
39
46
|
# [verifier] an API checking a bearer it was handed
|
|
40
47
|
# [resource] an API publishing what it is and which scopes it takes
|
|
41
48
|
# [handshake] an app registering itself, once, without a copied secret
|
|
42
|
-
#
|
|
43
|
-
# Everything reachable from here talks HTTP to a masks issuer and holds no
|
|
44
|
-
# state of its own beyond the discovery cache in ::registry.
|
|
45
49
|
module Client
|
|
46
50
|
class << self
|
|
51
|
+
attr_writer :plain_http
|
|
52
|
+
|
|
53
|
+
def plain_http?
|
|
54
|
+
@plain_http == true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def embed(app, &serves)
|
|
58
|
+
@embedded = app && Embedded.new(app, &serves)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def embedded_for(uri)
|
|
62
|
+
@embedded if @embedded&.serves?(uri)
|
|
63
|
+
end
|
|
64
|
+
|
|
47
65
|
# The process-wide cache of resolved issuers, so discovery is fetched
|
|
48
66
|
# once rather than per request.
|
|
49
67
|
#
|
|
@@ -57,7 +75,6 @@ module Masks
|
|
|
57
75
|
# cheap after the first.
|
|
58
76
|
#
|
|
59
77
|
# issuer = Masks::Client.issuer("https://auth.example")
|
|
60
|
-
# issuer.authorization_url(client_id: id, redirect_uri: uri)
|
|
61
78
|
#
|
|
62
79
|
# @return [Masks::Client::Issuer]
|
|
63
80
|
def issuer(url, **options)
|