masks 0.7.0 → 0.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +145 -100
- data/app/controllers/masks/rails/handshakes_controller.rb +11 -0
- data/app/controllers/masks/rails/sessions_controller.rb +39 -2
- data/lib/masks/client/claims.rb +69 -0
- data/lib/masks/client/errors.rb +8 -3
- data/lib/masks/client/introspection.rb +6 -0
- data/lib/masks/client/logout.rb +1 -1
- data/lib/masks/client/pkce.rb +1 -3
- data/lib/masks/client/proof.rb +153 -0
- data/lib/masks/client/rack.rb +15 -1
- data/lib/masks/client/registry.rb +0 -4
- data/lib/masks/client/resource.rb +31 -9
- data/lib/masks/client/session.rb +69 -10
- data/lib/masks/client/tokens.rb +1 -0
- data/lib/masks/client/verifier.rb +18 -7
- data/lib/masks/client.rb +1 -0
- data/lib/masks/rails/authentication.rb +86 -13
- data/lib/masks/rails/configuration.rb +13 -14
- data/lib/masks/rails/protected_resource.rb +10 -4
- data/lib/masks/rails.rb +2 -2
- data/lib/masks/version.rb +1 -1
- data/lib/masks.rb +2 -2
- metadata +4 -2
|
@@ -7,7 +7,15 @@ module Masks
|
|
|
7
7
|
included do
|
|
8
8
|
if respond_to?(:helper_method)
|
|
9
9
|
helper_method :masks_signed_in?, :masks_identity, :masks_tenant, :masks_scopes,
|
|
10
|
-
:masks_claims
|
|
10
|
+
:masks_claims, :masks_organization, :masks_organizations, :masks_role?,
|
|
11
|
+
:masks_login_url
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class_methods do
|
|
16
|
+
def masks_members_only!(role: nil, organization: nil, **options)
|
|
17
|
+
before_action(-> { authorize_masks_member!(*Array(role), organization: organization) },
|
|
18
|
+
**options.slice(:only, :except))
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
|
|
@@ -43,6 +51,7 @@ module Masks
|
|
|
43
51
|
session[masks_config.session_key] = held.compact
|
|
44
52
|
@masks_tokens = tokens
|
|
45
53
|
@masks_identity = held["identity"]
|
|
54
|
+
@masks_claims = nil
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def masks_held
|
|
@@ -51,7 +60,8 @@ module Masks
|
|
|
51
60
|
|
|
52
61
|
IDENTITY = [
|
|
53
62
|
"sub", "name", "preferred_username", "email", "email_verified", "tenant",
|
|
54
|
-
"picture", Masks::Client::Claims::AVATARS
|
|
63
|
+
"picture", Masks::Client::Claims::AVATARS, Masks::Client::Claims::ORGANIZATION,
|
|
64
|
+
Masks::Client::Claims::ORGANIZATIONS
|
|
55
65
|
].freeze
|
|
56
66
|
|
|
57
67
|
def masks_identity_from(tokens)
|
|
@@ -64,6 +74,7 @@ module Masks
|
|
|
64
74
|
session.delete(masks_config.session_key)
|
|
65
75
|
@masks_tokens = nil
|
|
66
76
|
@masks_identity = nil
|
|
77
|
+
@masks_claims = nil
|
|
67
78
|
end
|
|
68
79
|
|
|
69
80
|
def masks_signed_in?
|
|
@@ -122,6 +133,20 @@ module Masks
|
|
|
122
133
|
@masks_claims ||= Masks::Client::Claims.new(masks_identity || {})
|
|
123
134
|
end
|
|
124
135
|
|
|
136
|
+
def masks_organization
|
|
137
|
+
held = masks_claims.organization
|
|
138
|
+
|
|
139
|
+
held.present? ? held : nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def masks_organizations
|
|
143
|
+
masks_claims.organizations
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def masks_role?(*roles)
|
|
147
|
+
masks_organization&.role?(*roles) || false
|
|
148
|
+
end
|
|
149
|
+
|
|
125
150
|
def masks_scopes
|
|
126
151
|
masks_tokens&.scopes || []
|
|
127
152
|
end
|
|
@@ -138,12 +163,12 @@ module Masks
|
|
|
138
163
|
return false unless masks_configured?
|
|
139
164
|
return false if masks_tokens&.refresh_token.nil?
|
|
140
165
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
resource: masks_config.resource_for(request)
|
|
145
|
-
)
|
|
166
|
+
refreshed = masks_session.refresh(
|
|
167
|
+
masks_tokens.refresh_token,
|
|
168
|
+
resource: masks_config.resource_for(request)
|
|
146
169
|
)
|
|
170
|
+
|
|
171
|
+
masks_store(refreshed, identity: masks_refreshed_identity(refreshed))
|
|
147
172
|
true
|
|
148
173
|
rescue Masks::Client::Unregistered
|
|
149
174
|
masks_disconnect!
|
|
@@ -153,6 +178,16 @@ module Masks
|
|
|
153
178
|
false
|
|
154
179
|
end
|
|
155
180
|
|
|
181
|
+
def masks_refreshed_identity(tokens)
|
|
182
|
+
held = masks_held["identity"]
|
|
183
|
+
|
|
184
|
+
return nil unless held.is_a?(Hash)
|
|
185
|
+
|
|
186
|
+
profile = masks_session.profile(tokens)
|
|
187
|
+
|
|
188
|
+
profile.empty? ? nil : held.merge(profile.slice(*IDENTITY))
|
|
189
|
+
end
|
|
190
|
+
|
|
156
191
|
def masks_logout_url(return_to: nil)
|
|
157
192
|
return nil unless masks_configured?
|
|
158
193
|
|
|
@@ -168,11 +203,14 @@ module Masks
|
|
|
168
203
|
"#{request.base_url}#{masks_local_path(masks_config.after_sign_out) || '/'}"
|
|
169
204
|
end
|
|
170
205
|
|
|
171
|
-
def masks_login_url(return_to: nil)
|
|
206
|
+
def masks_login_url(return_to: nil, organization: nil)
|
|
172
207
|
path = Masks::Rails::Engine.routes.url_helpers.start_path
|
|
173
|
-
|
|
208
|
+
query = {
|
|
209
|
+
"return_to" => masks_local_path(return_to),
|
|
210
|
+
"organization" => Masks::Client::Session.organization_key(organization)
|
|
211
|
+
}.compact
|
|
174
212
|
|
|
175
|
-
|
|
213
|
+
query.empty? ? path : "#{path}?#{URI.encode_www_form(query)}"
|
|
176
214
|
end
|
|
177
215
|
|
|
178
216
|
def masks_account
|
|
@@ -188,11 +226,20 @@ module Masks
|
|
|
188
226
|
"picture" => masks_claims.picture,
|
|
189
227
|
"avatars" => masks_claims.avatars.to_h.presence,
|
|
190
228
|
"tenant" => masks_tenant,
|
|
229
|
+
"organization" => masks_organization&.to_h,
|
|
230
|
+
"organizations" => masks_organizations.map(&:to_h).presence,
|
|
191
231
|
"scopes" => masks_scopes,
|
|
192
|
-
"expires_at" => masks_tokens&.expires_at
|
|
232
|
+
"expires_at" => masks_tokens&.expires_at,
|
|
233
|
+
"account_url" => masks_account_url
|
|
193
234
|
}.compact
|
|
194
235
|
end
|
|
195
236
|
|
|
237
|
+
def masks_account_url
|
|
238
|
+
"#{masks_config.issuer_for(request).to_s.chomp('/')}/"
|
|
239
|
+
rescue Masks::Rails::Configuration::Unconfigured
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
196
243
|
def authenticate_masks!
|
|
197
244
|
return true if masks_configured? && (masks_signed_in? || (masks_tokens && masks_refresh!))
|
|
198
245
|
|
|
@@ -200,8 +247,28 @@ module Masks
|
|
|
200
247
|
false
|
|
201
248
|
end
|
|
202
249
|
|
|
250
|
+
def authorize_masks_member!(*roles, organization: nil)
|
|
251
|
+
return false unless authenticate_masks!
|
|
252
|
+
|
|
253
|
+
masks_claims.member!(*roles, organization: organization)
|
|
254
|
+
true
|
|
255
|
+
rescue Masks::Client::Forbidden => e
|
|
256
|
+
masks_forbid(e)
|
|
257
|
+
false
|
|
258
|
+
end
|
|
259
|
+
|
|
203
260
|
private
|
|
204
261
|
|
|
262
|
+
def masks_forbid(error)
|
|
263
|
+
response.headers["Cache-Control"] = "no-store"
|
|
264
|
+
|
|
265
|
+
if masks_wants_json?
|
|
266
|
+
render json: { "error" => error.code, "error_description" => error.description }, status: :forbidden
|
|
267
|
+
else
|
|
268
|
+
render plain: error.description, status: :forbidden
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
205
272
|
def masks_refuse
|
|
206
273
|
return masks_refuse_json if masks_wants_json?
|
|
207
274
|
return redirect_to(masks_handshake_path) unless masks_configured?
|
|
@@ -251,9 +318,15 @@ module Masks
|
|
|
251
318
|
path = value.to_s
|
|
252
319
|
|
|
253
320
|
return nil unless path.start_with?("/")
|
|
254
|
-
return nil if path.
|
|
321
|
+
return nil if path.match?(/[\x00-\x20\x7f\\]/)
|
|
255
322
|
|
|
256
|
-
path
|
|
323
|
+
uri = URI.parse(path)
|
|
324
|
+
|
|
325
|
+
return nil if uri.scheme || uri.host || uri.userinfo || !uri.path.start_with?("/") || uri.path.start_with?("//")
|
|
326
|
+
|
|
327
|
+
[ uri.path, uri.query ].compact.join("?")
|
|
328
|
+
rescue URI::InvalidURIError
|
|
329
|
+
nil
|
|
257
330
|
end
|
|
258
331
|
end
|
|
259
332
|
end
|
|
@@ -7,7 +7,7 @@ module Masks
|
|
|
7
7
|
:after_sign_out, :session_key, :sign_out_of_issuer, :parent_controller,
|
|
8
8
|
:credentials_path, :authenticate_everything, :delegates
|
|
9
9
|
attr_writer :issuer, :redirect_uri, :name, :credentials, :store, :forget, :logged_out,
|
|
10
|
-
:delegation_redirect_uri
|
|
10
|
+
:delegation_redirect_uri, :organization, :manages
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@scope = Masks::Client::Session::DEFAULT_SCOPE
|
|
@@ -51,6 +51,10 @@ module Masks
|
|
|
51
51
|
resolve(@resource, request)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def organization_for(request)
|
|
55
|
+
resolve(@organization, request)
|
|
56
|
+
end
|
|
57
|
+
|
|
54
58
|
def credentials_for(request)
|
|
55
59
|
held = @credentials ? resolve(@credentials, request) : default_credentials.read
|
|
56
60
|
held ||= {}
|
|
@@ -85,6 +89,14 @@ module Masks
|
|
|
85
89
|
true
|
|
86
90
|
end
|
|
87
91
|
|
|
92
|
+
def manages?(request, identity = nil)
|
|
93
|
+
return false unless @manages.respond_to?(:call)
|
|
94
|
+
|
|
95
|
+
held = @manages.arity == 1 ? @manages.call(request) : @manages.call(request, identity)
|
|
96
|
+
|
|
97
|
+
held ? true : false
|
|
98
|
+
end
|
|
99
|
+
|
|
88
100
|
def can_forget?
|
|
89
101
|
@forget.respond_to?(:call) || !@store.respond_to?(:call)
|
|
90
102
|
end
|
|
@@ -102,19 +114,6 @@ module Masks
|
|
|
102
114
|
@store.call(request, registration)
|
|
103
115
|
end
|
|
104
116
|
|
|
105
|
-
MINIMUM_ISSUER = 1
|
|
106
|
-
|
|
107
|
-
def issuer_speaks!(request)
|
|
108
|
-
spoken = Masks::Client::Issuer.resolve(issuer_for(request))
|
|
109
|
-
.discovery["masks_protocol_version"].to_i
|
|
110
|
-
|
|
111
|
-
return true if spoken >= MINIMUM_ISSUER
|
|
112
|
-
|
|
113
|
-
raise Unconfigured,
|
|
114
|
-
"#{issuer_for(request)} speaks masks protocol #{spoken}, and masks " \
|
|
115
|
-
"#{Masks::VERSION} needs at least #{MINIMUM_ISSUER}"
|
|
116
|
-
end
|
|
117
|
-
|
|
118
117
|
def session_for(request)
|
|
119
118
|
raise Unconfigured, "this app has not shaken hands with #{issuer_for(request)}" unless configured?(request)
|
|
120
119
|
|
|
@@ -18,19 +18,25 @@ module Masks
|
|
|
18
18
|
@masks_claims if defined?(@masks_claims)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def masks_authenticate!(scope: nil, **)
|
|
22
|
-
@masks_claims = masks_resource.authenticate(request.authorization, scope: scope
|
|
21
|
+
def masks_authenticate!(scope: nil, role: nil, organization: nil, **)
|
|
22
|
+
@masks_claims = masks_resource.authenticate(request.authorization, scope: scope, role: role,
|
|
23
|
+
organization: organization, **masks_proof)
|
|
23
24
|
rescue Masks::Client::Challenge => e
|
|
24
25
|
masks_challenge(e)
|
|
25
26
|
false
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def masks_authenticate(scope: nil)
|
|
29
|
-
masks_resource.authenticate(request.authorization, scope: scope
|
|
29
|
+
def masks_authenticate(scope: nil, role: nil, organization: nil)
|
|
30
|
+
masks_resource.authenticate(request.authorization, scope: scope, role: role, organization: organization,
|
|
31
|
+
**masks_proof)
|
|
30
32
|
rescue Masks::Client::Unauthenticated
|
|
31
33
|
nil
|
|
32
34
|
end
|
|
33
35
|
|
|
36
|
+
def masks_proof
|
|
37
|
+
{ proof: request.headers["DPoP"], method: request.request_method, url: "#{request.base_url}#{request.path}" }
|
|
38
|
+
end
|
|
39
|
+
|
|
34
40
|
def masks_challenge(error)
|
|
35
41
|
error = Masks::Client::Unauthorized.new(error.to_s) unless error.is_a?(Masks::Client::Challenge)
|
|
36
42
|
|
data/lib/masks/rails.rb
CHANGED
|
@@ -20,8 +20,8 @@ module Masks
|
|
|
20
20
|
# [ProtectedResource] the other direction — checking a bearer this app was
|
|
21
21
|
# handed, for an API rather than a browser
|
|
22
22
|
#
|
|
23
|
-
# This engine is the client.
|
|
24
|
-
#
|
|
23
|
+
# This engine is the client. The provider is the masks-server gem, whether
|
|
24
|
+
# it runs elsewhere or is mounted on a subdomain of this app.
|
|
25
25
|
module Rails
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/masks/version.rb
CHANGED
data/lib/masks.rb
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
# issuer = Masks::Client.issuer("https://auth.example")
|
|
11
11
|
# issuer.authorization_url(client_id: id, redirect_uri: uri)
|
|
12
12
|
#
|
|
13
|
-
# The provider itself is not in here. It is
|
|
14
|
-
#
|
|
13
|
+
# The provider itself is not in here. It is the masks-server gem, which runs
|
|
14
|
+
# as an app of its own or mounted inside another Rails app.
|
|
15
15
|
module Masks
|
|
16
16
|
end
|
|
17
17
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: masks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- the masks authors
|
|
@@ -25,7 +25,8 @@ dependencies:
|
|
|
25
25
|
version: '3.1'
|
|
26
26
|
description: Discovery, PKCE authorization, token exchange, and token verification
|
|
27
27
|
against a masks issuer, with Rack middleware for a resource server and a Rails engine
|
|
28
|
-
that mounts the consumer half of the code flow. The auth server itself
|
|
28
|
+
that mounts the consumer half of the code flow. The auth server itself is the masks-server
|
|
29
|
+
gem.
|
|
29
30
|
executables: []
|
|
30
31
|
extensions: []
|
|
31
32
|
extra_rdoc_files: []
|
|
@@ -56,6 +57,7 @@ files:
|
|
|
56
57
|
- lib/masks/client/issuer.rb
|
|
57
58
|
- lib/masks/client/logout.rb
|
|
58
59
|
- lib/masks/client/pkce.rb
|
|
60
|
+
- lib/masks/client/proof.rb
|
|
59
61
|
- lib/masks/client/rack.rb
|
|
60
62
|
- lib/masks/client/registration.rb
|
|
61
63
|
- lib/masks/client/registry.rb
|