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.
@@ -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
- masks_store(
142
- masks_session.refresh(
143
- masks_tokens.refresh_token,
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
- target = masks_local_path(return_to)
208
+ query = {
209
+ "return_to" => masks_local_path(return_to),
210
+ "organization" => Masks::Client::Session.organization_key(organization)
211
+ }.compact
174
212
 
175
- target ? "#{path}?return_to=#{CGI.escape(target)}" : path
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.start_with?("//", "/\\")
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. It never runs in the same process as the
24
- # provider, which is a standalone deployable holding its own database.
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
@@ -1,3 +1,3 @@
1
1
  module Masks
2
- VERSION = "0.7.0".freeze
2
+ VERSION = "0.9.1".freeze
3
3
  end
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 a deployable that holds a database
14
- # and the per-tenant signing keys, and it stays standalone.
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.7.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 stays standalone.
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