masks-server 0.8.0 → 0.9.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/masks/server/client_authentication.rb +1 -1
  3. data/app/controllers/concerns/masks/server/resource_token.rb +12 -3
  4. data/app/controllers/masks/server/application_controller.rb +8 -6
  5. data/app/controllers/masks/server/authorize_controller.rb +1 -1
  6. data/app/controllers/masks/server/logins_controller.rb +2 -1
  7. data/app/controllers/masks/server/registrations_controller.rb +9 -1
  8. data/app/controllers/masks/server/scim/metadata_controller.rb +1 -1
  9. data/app/controllers/masks/server/tokens_controller.rb +1 -1
  10. data/app/graphql/masks/server/manage/mutations/add_app.rb +1 -1
  11. data/app/helpers/masks/server/account_helper.rb +1 -1
  12. data/app/models/masks/server/actor.rb +3 -3
  13. data/app/models/masks/server/client.rb +46 -6
  14. data/app/models/masks/server/handshake.rb +12 -6
  15. data/app/models/masks/server/login.rb +3 -6
  16. data/app/models/masks/server/pending_handshake.rb +2 -1
  17. data/app/models/masks/server/session.rb +7 -5
  18. data/app/policies/masks/server/exchange_policy.rb +1 -1
  19. data/app/policies/masks/server/grant_checks.rb +1 -1
  20. data/app/views/masks/server/account/_info.html.erb +1 -1
  21. data/app/views/masks/server/account/_organizations.html.erb +4 -4
  22. data/app/views/masks/server/application/_actor.html.erb +27 -0
  23. data/app/views/masks/server/handshakes/refused.html.erb +1 -1
  24. data/app/views/masks/server/handshakes/show.html.erb +1 -1
  25. data/app/views/masks/server/logins/_choose_organization.html.erb +1 -1
  26. data/app/views/masks/server/logins/_consent.html.erb +1 -1
  27. data/app/views/masks/server/sessions/confirm.html.erb +1 -1
  28. data/config/locales/de/account.yml +2 -2
  29. data/config/locales/de/application.yml +1 -1
  30. data/config/locales/de/events.yml +5 -5
  31. data/config/locales/de/manage/groups.yml +1 -1
  32. data/config/locales/de/manage/organization.yml +1 -1
  33. data/config/locales/en/account.yml +2 -2
  34. data/config/locales/en/application.yml +1 -1
  35. data/config/locales/en/events.yml +5 -5
  36. data/config/locales/en/manage/groups.yml +1 -1
  37. data/config/locales/en/manage/organization.yml +1 -1
  38. data/config/locales/es/account.yml +2 -2
  39. data/config/locales/es/application.yml +1 -1
  40. data/config/locales/es/events.yml +5 -5
  41. data/config/locales/es/manage/groups.yml +1 -1
  42. data/config/locales/es/manage/organization.yml +1 -1
  43. data/config/locales/fr/account.yml +2 -2
  44. data/config/locales/fr/application.yml +1 -1
  45. data/config/locales/fr/events.yml +5 -5
  46. data/config/locales/fr/manage/groups.yml +1 -1
  47. data/config/locales/fr/manage/organization.yml +1 -1
  48. data/db/migrate/20261013010000_add_handshake_return_to_to_clients.rb +5 -0
  49. data/lib/generators/masks/server/install/install_generator.rb +2 -2
  50. data/lib/masks/server/browser_access.rb +35 -0
  51. data/lib/masks/server/configuration.rb +3 -3
  52. data/lib/masks/server/engine.rb +1 -0
  53. data/lib/masks/server/schema_reference.rb +17 -4
  54. data/lib/masks/server.rb +1 -0
  55. data/public/masks-assets/.vite/manifest.json +7 -7
  56. data/public/masks-assets/assets/app-B27oPUll.css +2 -0
  57. data/public/masks-assets/assets/{initial-D99AzFOd.js → initial-C1GpCiEh.js} +2 -2
  58. data/public/masks-assets/assets/{initial-D99AzFOd.js.map → initial-C1GpCiEh.js.map} +1 -1
  59. data/public/masks-assets/assets/login-C_6igRZ3.js +4 -0
  60. data/public/masks-assets/assets/login-C_6igRZ3.js.map +1 -0
  61. data/public/masks-assets/assets/manage-Dbfiy3Mj.js +567 -0
  62. data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +1 -0
  63. data/version.txt +1 -1
  64. metadata +11 -9
  65. data/app/views/masks/server/application/_person.html.erb +0 -27
  66. data/public/masks-assets/assets/app-DoQZrM_J.css +0 -2
  67. data/public/masks-assets/assets/login-CNs1UpxJ.js +0 -4
  68. data/public/masks-assets/assets/login-CNs1UpxJ.js.map +0 -1
  69. data/public/masks-assets/assets/manage-isLCrWiU.js +0 -567
  70. data/public/masks-assets/assets/manage-isLCrWiU.js.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0815510a0ee659d9ba2cf35bb8840d7a3a7b7abb7306e0e0cf3f64fd7890d7dd'
4
- data.tar.gz: 188b4436824c6f113da49a7880ad15d6f4d0c915f5c5df9b94ba7affcb51fca8
3
+ metadata.gz: 36e33f34e755bbbb4470a5c55b920624f2042678b87c810ce614e86f9638a4df
4
+ data.tar.gz: 2bf178807ba578015922b44145ad0eb28454698adb877ac51b5059be57a35ca3
5
5
  SHA512:
6
- metadata.gz: faeda2a8132f3e2e92ce8a1f41d235cae52f0640ad33ed3220d526619c5ecd113fece66af5923cb8e610564399ba3db0f22bdd16e9e0306d0155ba63f9e13f91
7
- data.tar.gz: bef27fdefa6bc7d1f88a386ac23261cad8b487137ea8130c0bfa41467f7bbec11b59058386ceb0fabe0f97cca1a914a1a3a9aaef6c74a48478b266e3a9c62858
6
+ metadata.gz: 3d32dd6592c56c0132971c1f322ca90b2a31df6832c0b9fe33d5c41c7d5d7aab443b4c6b2fec0a9d3ac0635b22b6d86bbda1c451bd791fd3794e4776c1edd1c8
7
+ data.tar.gz: 59f5658c277590947cf74f3b8e57fe2d9d7b01f1a160bdc3bdbc3593f4aba9b9395b08255b866e54029f48aff6d9b7b1384daf9ab1b21b95f7468678a800fbcb
@@ -44,7 +44,7 @@ module Masks
44
44
  end
45
45
 
46
46
  def assertion_audiences
47
- [ issuer.url, "#{issuer.url}/token", "#{issuer.url}#{request.path}" ].uniq
47
+ [ issuer.url, "#{issuer.url}/token", "#{issuer.url}#{request.path_info}" ].uniq
48
48
  end
49
49
 
50
50
  def presented_credentials
@@ -19,7 +19,7 @@ module Masks
19
19
  answer = handler.call(request.env)
20
20
 
21
21
  return render_rack(answer) unless answer.first == 200
22
- return refuse_token("a bearer token is required") if token.nil?
22
+ return ask_for_token if token.nil?
23
23
 
24
24
  yield token
25
25
  end
@@ -50,8 +50,11 @@ module Masks
50
50
 
51
51
  def decode(bearer)
52
52
  claims_in_token(bearer.access_token)
53
+ rescue JWT::ExpiredSignature
54
+ bearer.invalid_token!("that token has expired")
53
55
  rescue JWT::DecodeError => e
54
- bearer.invalid_token!(e.message)
56
+ ::Rails.logger.info("masks: an access token was refused: #{e.message}")
57
+ bearer.invalid_token!("that token is not one this server signed")
55
58
  end
56
59
 
57
60
  def claims_in_token(secret)
@@ -96,7 +99,7 @@ module Masks
96
99
  end
97
100
 
98
101
  def proof_for(secret = nil)
99
- Proof.read!(request, url: "#{issuer.url}#{request.path}", access_token: secret)
102
+ Proof.read!(request, url: "#{issuer.url}#{request.path_info}", access_token: secret)
100
103
  end
101
104
 
102
105
  def held_token(secret)
@@ -118,6 +121,12 @@ module Masks
118
121
  nil
119
122
  end
120
123
 
124
+ def ask_for_token
125
+ response.headers["WWW-Authenticate"] = %(Bearer realm="#{issuer.url}")
126
+
127
+ head :unauthorized
128
+ end
129
+
121
130
  def refuse_token(description)
122
131
  render_rack(
123
132
  Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
@@ -143,7 +143,7 @@ module Masks
143
143
  Current.device = device
144
144
  end
145
145
 
146
- def sign_in(actor, amr: [], policy: nil, first_factor: {})
146
+ def sign_in(actor, amr: [], policy: nil, first_factor: {}, authenticated_at: nil)
147
147
  carried = session.to_hash.slice(REQUESTS, HANDSHAKES, "login", RETURN_TO)
148
148
  reset_session
149
149
  carried.each { |key, value| session[key] = value }
@@ -157,7 +157,8 @@ module Masks
157
157
  ip_address: request.remote_ip,
158
158
  amr: amr,
159
159
  policy: policy,
160
- first_factor: first_factor
160
+ first_factor: first_factor,
161
+ authenticated_at: authenticated_at
161
162
  )
162
163
 
163
164
  cookies.encrypted[:masks_session] = {
@@ -301,17 +302,18 @@ module Masks
301
302
  end
302
303
 
303
304
  def settle!(login)
304
- hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by)
305
+ hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
306
+ authenticated_at: login.factored_at(:first_factor))
305
307
  forget_login
306
308
  end
307
309
 
308
- def hold(actor, amr: [], policy: nil, first_factor: {})
310
+ def hold(actor, amr: [], policy: nil, first_factor: {}, authenticated_at: nil)
309
311
  return if actor.nil?
310
- return current_session.first_factored!(first_factor) if current_session&.actor_id == actor.id
312
+ return current_session.first_factored!(first_factor, at: authenticated_at) if current_session&.actor_id == actor.id
311
313
 
312
314
  sign_out if current_session
313
315
 
314
- sign_in(actor, amr: amr, policy: policy, first_factor: first_factor)
316
+ sign_in(actor, amr: amr, policy: policy, first_factor: first_factor, authenticated_at: authenticated_at)
315
317
  end
316
318
 
317
319
  def policy_denied(denial)
@@ -63,7 +63,7 @@ module Masks
63
63
  req.bad_request!(:invalid_client, unknown_client(req.client_id)) if client.nil?
64
64
  req.invalid_request!("redirect_uri is required") if req.redirect_uri.blank?
65
65
 
66
- req.verify_redirect_uri!(client.redirect_uris)
66
+ req.verify_redirect_uri!(client.redirect_uri?(req.redirect_uri) ? [ req.redirect_uri.to_s ] : client.redirect_uris)
67
67
  req.verified_redirect_uri = with_issuer(req.verified_redirect_uri)
68
68
  res.redirect_uri = req.verified_redirect_uri
69
69
 
@@ -151,7 +151,8 @@ module Masks
151
151
  return unless login.actor
152
152
 
153
153
  if login.touched?(:first_factor)
154
- sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by)
154
+ sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
155
+ authenticated_at: login.factored_at(:first_factor))
155
156
  elsif login.signed_in? && login.touched?(:second_factor)
156
157
  current_session.update!(amr: Array(current_session.amr) | login.amr)
157
158
  end
@@ -133,6 +133,12 @@ module Masks
133
133
  }, status: :bad_request
134
134
  end
135
135
 
136
+ def ask_for_token
137
+ response.headers["WWW-Authenticate"] = %(Bearer realm="#{issuer.url}")
138
+
139
+ head :unauthorized
140
+ end
141
+
136
142
  def issued(client)
137
143
  client.metadata.merge(
138
144
  "client_secret" => client.secret,
@@ -155,7 +161,9 @@ module Masks
155
161
  end
156
162
 
157
163
  def unauthorized(description)
158
- response.headers["WWW-Authenticate"] = %(Bearer error="invalid_token")
164
+ return ask_for_token if bearer.blank?
165
+
166
+ response.headers["WWW-Authenticate"] = %(Bearer realm="#{issuer.url}", error="invalid_token")
159
167
 
160
168
  render json: {
161
169
  "error" => "invalid_token",
@@ -7,7 +7,7 @@ module Masks
7
7
  def service_provider_config
8
8
  scim({
9
9
  "schemas" => [ Scim::SERVICE_PROVIDER_CONFIG ],
10
- "documentationUri" => "#{::Rails.configuration.masks.docs_url}/concepts/provisioning/",
10
+ "documentationUri" => "#{::Rails.configuration.masks.docs_url}/guides/sso-saml/#provisioning",
11
11
  "patch" => { "supported" => true },
12
12
  "bulk" => { "supported" => false, "maxOperations" => 0, "maxPayloadSize" => 0 },
13
13
  "filter" => { "supported" => true, "maxResults" => Scim::MAX_RESULTS },
@@ -42,7 +42,7 @@ module Masks
42
42
  return @proof if defined?(@proof)
43
43
 
44
44
  @proof = if Proof.presented?(request)
45
- Proof.read!(request, url: "#{issuer.url}#{request.path}")
45
+ Proof.read!(request, url: "#{issuer.url}#{request.path_info}")
46
46
  end
47
47
  end
48
48
 
@@ -7,7 +7,7 @@ module Masks
7
7
 
8
8
  argument :preset, ID
9
9
  argument :base_url, String, description: "Where the app is served, such as https://grafana.example.com."
10
- argument :name, String, required: false, description: "What people see. The preset's name when left out."
10
+ argument :name, String, required: false, description: "What people see."
11
11
  argument :slug, String, required: false,
12
12
  description: "The name some apps put in their redirect URI, such as Gitea's authentication source."
13
13
 
@@ -15,7 +15,7 @@ module Masks
15
15
  close: '<path d="M6.5 6.5l7 7M13.5 6.5l-7 7"/>',
16
16
  check: '<path d="m5.5 10.5 3 3 6-7"/>',
17
17
  organization: '<rect x="3.5" y="7" width="13" height="9.5" rx="1.5"/><path d="M7.5 7V4.5h5V7"/><path d="M3.5 11h13"/>',
18
- person: '<circle cx="10" cy="7" r="3.25"/><path d="M4 16.5c.8-3 3.2-4.5 6-4.5s5.2 1.5 6 4.5"/>',
18
+ actor: '<circle cx="10" cy="7" r="3.25"/><path d="M4 16.5c.8-3 3.2-4.5 6-4.5s5.2 1.5 6 4.5"/>',
19
19
  authenticator: '<rect x="3" y="5.5" width="14" height="9" rx="2"/><path d="M6.5 10h.01M10 10h.01M13.5 10h.01"/>',
20
20
  backup_codes: '<path d="M5.5 3.5h9v13h-9z"/><path d="M8 7h4M8 10h4M8 13h4"/>',
21
21
  manage: '<path d="M10 2.5 3.5 5v5c0 3.4 2.7 6.2 6.5 7.5 3.8-1.3 6.5-4.1 6.5-7.5V5L10 2.5Z"/>'
@@ -190,14 +190,14 @@ module Masks
190
190
  holds?(Scopes::MANAGE)
191
191
  end
192
192
 
193
- def person_json
193
+ def card_json
194
194
  style = Avatars.held?(self) ? Avatars::PHOTO : Avatars::FALLBACK
195
195
 
196
196
  {
197
197
  "name" => display_name,
198
198
  "details" => display_details,
199
- "note" => (I18n.t("application.person.unconfirmed") if email_unconfirmed?),
200
- "role" => (I18n.t("application.person.manager") if manages?),
199
+ "note" => (I18n.t("application.actor.unconfirmed") if email_unconfirmed?),
200
+ "role" => (I18n.t("application.actor.manager") if manages?),
201
201
  "avatar" => Avatars.url(self, style, subject: uuid, size: 128)
202
202
  }
203
203
  end
@@ -123,7 +123,7 @@ module Masks
123
123
  {
124
124
  name: handshake.name,
125
125
  redirect_uris: handshake.redirect_uris,
126
- post_logout_redirect_uris: [ handshake.return_to ].compact,
126
+ post_logout_redirect_uris: [ handshake.post_logout_redirect_uri ].compact,
127
127
  resources: [ handshake.resource ],
128
128
  allowed_scopes: Scopes.join(handshake.scopes),
129
129
  grant_types: handshake.grant_types,
@@ -131,6 +131,7 @@ module Masks
131
131
  token_endpoint_auth_method: handshake.auth_method,
132
132
  backchannel_logout_uri: handshake.backchannel_logout_uri,
133
133
  client_uri: handshake.origin,
134
+ handshake_return_to: handshake.return_to,
134
135
  dynamic: false
135
136
  }
136
137
  end
@@ -169,8 +170,7 @@ module Masks
169
170
  name: held[:name] || name,
170
171
  grant_types: grant_types,
171
172
  response_types: held[:response_types].presence || response_types_for(grant_types),
172
- allowed_scopes: Scopes.join(bounded(metadata.to_h.stringify_keys["scope"].presence ||
173
- Current.tenant&.dynamic_client_ceiling || DEFAULT_SCOPES)),
173
+ allowed_scopes: Scopes.join(declared_scopes(metadata)),
174
174
  dynamic: true
175
175
  )
176
176
  end
@@ -206,6 +206,12 @@ module Masks
206
206
  ).issue_credentials!
207
207
  end
208
208
 
209
+ def declared_scopes(metadata)
210
+ declared = metadata.to_h.stringify_keys["scope"]
211
+
212
+ Scopes.list(declared).empty? ? [] : bounded(declared)
213
+ end
214
+
209
215
  def bounded(requested)
210
216
  reserved = Scopes.reserved(requested) + Scopes.list(requested).select { |s| Scopes.prefix?(s) }
211
217
 
@@ -380,7 +386,7 @@ module Masks
380
386
  end
381
387
 
382
388
  def redirect_uri?(candidate)
383
- redirect_uris.include?(candidate.to_s)
389
+ redirect_uris.include?(candidate.to_s) || redirect_uris.any? { |registered| same_loopback?(registered, candidate.to_s) }
384
390
  end
385
391
 
386
392
  def grants?(grant_type)
@@ -424,7 +430,11 @@ module Masks
424
430
  end
425
431
 
426
432
  def scope_list
427
- Scopes.union(required_scopes, allowed_scopes)
433
+ Scopes.union(required_scopes, offered_scopes)
434
+ end
435
+
436
+ def follows_registration?
437
+ dynamic? && Scopes.list(allowed_scopes).empty?
428
438
  end
429
439
 
430
440
  def access_lifetime
@@ -442,7 +452,19 @@ module Masks
442
452
  def permitted_scopes(requested)
443
453
  return scope_list if Scopes.list(requested).empty?
444
454
 
445
- Scopes.union(required_scopes, Scopes.granted(requested, allowed_scopes))
455
+ return Scopes.union(required_scopes, unreserved(requested)) if open_to_anything?
456
+
457
+ Scopes.union(required_scopes, Scopes.granted(requested, offered_scopes))
458
+ end
459
+
460
+ def refused_scopes(requested)
461
+ Scopes.list(requested) - permitted_scopes(requested)
462
+ end
463
+
464
+ def offered_scopes
465
+ return Scopes.list(allowed_scopes) unless follows_registration?
466
+
467
+ Current.tenant&.dynamic_client_ceiling || DEFAULT_SCOPES
446
468
  end
447
469
 
448
470
  def metadata
@@ -544,6 +566,24 @@ module Masks
544
566
  self.class.loopback?(uri)
545
567
  end
546
568
 
569
+ def open_to_anything?
570
+ follows_registration? && Current.tenant&.dynamic_registration == Tenant::REGISTRATION_ANYTHING
571
+ end
572
+
573
+ def unreserved(requested)
574
+ Scopes.list(requested).reject { |scope| Scopes.prefix?(scope) || Scopes.reserved(scope).any? }
575
+ end
576
+
577
+ def same_loopback?(registered, candidate)
578
+ held = URI.parse(registered)
579
+ asked = URI.parse(candidate)
580
+
581
+ [ held, asked ].all? { |uri| uri.instance_of?(URI::HTTP) && LOOPBACK.include?(uri.hostname) && uri.userinfo.nil? && uri.fragment.nil? } &&
582
+ held.hostname == asked.hostname && held.path == asked.path && held.query == asked.query
583
+ rescue URI::InvalidURIError
584
+ false
585
+ end
586
+
547
587
  def grant_types_are_known
548
588
  unknown = grant_types - GRANT_TYPES
549
589
  errors.add(:grant_types, "not supported: #{unknown.join(', ')}") if unknown.any?
@@ -4,7 +4,7 @@ module Masks
4
4
  GRANT_TYPES = %w[authorization_code refresh_token].freeze
5
5
 
6
6
  attr_reader :name, :redirect_uris, :resource, :scopes, :return_to, :state, :auth_method,
7
- :backchannel_logout_uri
7
+ :backchannel_logout_uri, :post_logout_redirect_uri
8
8
 
9
9
  class << self
10
10
  def from_request(request)
@@ -19,7 +19,8 @@ module Masks
19
19
  return_to: params["return_to"],
20
20
  state: params["state"],
21
21
  auth_method: params["token_endpoint_auth_method"],
22
- backchannel_logout_uri: params["backchannel_logout_uri"]
22
+ backchannel_logout_uri: params["backchannel_logout_uri"],
23
+ post_logout_redirect_uri: params["post_logout_redirect_uri"]
23
24
  )
24
25
  end
25
26
 
@@ -36,7 +37,8 @@ module Masks
36
37
  return_to: row.redirect_uri,
37
38
  state: held["state"],
38
39
  auth_method: held["auth_method"],
39
- backchannel_logout_uri: held["backchannel_logout_uri"]
40
+ backchannel_logout_uri: held["backchannel_logout_uri"],
41
+ post_logout_redirect_uri: held["post_logout_redirect_uri"]
40
42
  )
41
43
  end
42
44
  end
@@ -54,7 +56,8 @@ module Masks
54
56
  "state" => state,
55
57
  "redirect_uris" => redirect_uris.sort,
56
58
  "auth_method" => auth_method,
57
- "backchannel_logout_uri" => backchannel_logout_uri
59
+ "backchannel_logout_uri" => backchannel_logout_uri,
60
+ "post_logout_redirect_uri" => post_logout_redirect_uri
58
61
  }.compact
59
62
  end
60
63
 
@@ -73,12 +76,14 @@ module Masks
73
76
 
74
77
  pairs << [ "state", state ] if state
75
78
  pairs << [ "backchannel_logout_uri", backchannel_logout_uri ] if backchannel_logout_uri
79
+ pairs << [ "post_logout_redirect_uri", post_logout_redirect_uri ] if post_logout_redirect_uri
76
80
  redirect_uris.each { |uri| pairs << [ "redirect_uris", uri ] }
77
81
  pairs
78
82
  end
79
83
 
80
84
  def initialize(name: nil, redirect_uris: nil, resource: nil, scopes: nil,
81
- return_to: nil, state: nil, auth_method: nil, backchannel_logout_uri: nil)
85
+ return_to: nil, state: nil, auth_method: nil, backchannel_logout_uri: nil,
86
+ post_logout_redirect_uri: nil)
82
87
  @name = name.to_s.strip.presence || "An application"
83
88
  @redirect_uris = Array(redirect_uris).map(&:to_s).reject(&:empty?).uniq
84
89
  @resource = resource.to_s
@@ -87,6 +92,7 @@ module Masks
87
92
  @state = state.presence
88
93
  @auth_method = auth_method.to_s.presence || Client::DEFAULT_AUTH_METHOD
89
94
  @backchannel_logout_uri = backchannel_logout_uri.to_s.strip.presence
95
+ @post_logout_redirect_uri = post_logout_redirect_uri.to_s.strip.presence
90
96
  end
91
97
 
92
98
  def origin
@@ -134,7 +140,7 @@ module Masks
134
140
  return "token_endpoint_auth_method must be one of #{Client::AUTH_METHODS.join(', ')}"
135
141
  end
136
142
 
137
- elsewhere = ([ return_to ] + redirect_uris + Array(backchannel_logout_uri))
143
+ elsewhere = ([ return_to ] + redirect_uris + Array(backchannel_logout_uri) + Array(post_logout_redirect_uri))
138
144
  .reject { |uri| origin_of(uri) == origin }
139
145
  return "everything must share the origin #{origin}: #{elsewhere.join(', ')}" if elsewhere.any?
140
146
 
@@ -416,8 +416,7 @@ module Masks
416
416
  "rid" => rid,
417
417
  "docs" => ::Rails.configuration.masks.docs_url,
418
418
  "actor" => actor && { "nickname" => actor.nickname, "name" => actor.name,
419
- "identifier" => actor.identifier },
420
- "person" => person_json,
419
+ "identifier" => actor.identifier, "card" => card_json },
421
420
  "client" => client_json,
422
421
  "tenant" => tenant && {
423
422
  "name" => tenant.name, "logo" => tenant.logo_url
@@ -429,10 +428,8 @@ module Masks
429
428
  answering.reduce(base) { |json, state| state.enabled? ? json.merge(state.as_json) : json }
430
429
  end
431
430
 
432
- def person_json
433
- return nil if actor.nil? || authenticated_at.nil?
434
-
435
- actor.person_json
431
+ def card_json
432
+ actor.card_json if authenticated_at
436
433
  end
437
434
 
438
435
  def forget_vanished_actor!
@@ -19,7 +19,8 @@ module Masks
19
19
  "redirect_uris" => handshake.redirect_uris,
20
20
  "state" => handshake.state,
21
21
  "auth_method" => handshake.auth_method,
22
- "backchannel_logout_uri" => handshake.backchannel_logout_uri
22
+ "backchannel_logout_uri" => handshake.backchannel_logout_uri,
23
+ "post_logout_redirect_uri" => handshake.post_logout_redirect_uri
23
24
  }.compact
24
25
  )
25
26
  end
@@ -23,7 +23,7 @@ module Masks
23
23
 
24
24
  class << self
25
25
  def start!(actor:, device: nil, user_agent: nil, ip_address: nil, amr: [], origin: nil, policy: nil,
26
- first_factor: {})
26
+ first_factor: {}, authenticated_at: nil)
27
27
  secret = SecureRandom.urlsafe_base64(48)
28
28
  lifetime = policy&.session_lifetime&.seconds || LIFETIME
29
29
  now = Time.current
@@ -36,7 +36,7 @@ module Masks
36
36
  user_agent: user_agent,
37
37
  ip_address: ip_address,
38
38
  origin: origin.presence || Current.origin,
39
- authenticated_at: now,
39
+ authenticated_at: authenticated_at || now,
40
40
  last_seen_at: now,
41
41
  amr: Array(amr),
42
42
  first_factor: first_factor["factor"],
@@ -71,10 +71,12 @@ module Masks
71
71
  { "factor" => first_factor, "provider_id" => provider_id }.compact
72
72
  end
73
73
 
74
- def first_factored!(held)
75
- return if held["factor"].blank? || held == first_factored_by
74
+ def first_factored!(held, at: nil)
75
+ changes = {}
76
+ changes[:authenticated_at] = at if at.present? && (authenticated_at.nil? || at > authenticated_at)
77
+ changes.merge!(first_factor: held["factor"], provider_id: held["provider_id"]) if held["factor"].present? && held != first_factored_by
76
78
 
77
- update!(first_factor: held["factor"], provider_id: held["provider_id"])
79
+ update!(changes) if changes.any?
78
80
  end
79
81
 
80
82
  def bound?
@@ -85,7 +85,7 @@ module Masks
85
85
  end
86
86
 
87
87
  def scopes_stay_within_the_client
88
- beyond = Scopes.refused(client.scope_list, requested_scopes)
88
+ beyond = client.refused_scopes(requested_scopes)
89
89
 
90
90
  deny!("invalid_scope", "this client may not hold #{beyond.join(', ')}") if beyond.any?
91
91
  end
@@ -8,7 +8,7 @@ module Masks
8
8
  end
9
9
 
10
10
  def scopes_are_permitted
11
- refused = Scopes.refused(client.scope_list, requested_scopes)
11
+ refused = client.refused_scopes(requested_scopes)
12
12
 
13
13
  if refused.any?
14
14
  deny!("invalid_scope",
@@ -4,7 +4,7 @@
4
4
  <div class="entries">
5
5
  <details class="entry" id="profile">
6
6
  <summary>
7
- <span class="entry-icon"><%= account_icon(:person) %></span>
7
+ <span class="entry-icon"><%= account_icon(:actor) %></span>
8
8
  <span class="entry-label"><%= account_text("profile.heading") %></span>
9
9
  <span class="entry-state"><%= actor.name.presence || actor.nickname %></span>
10
10
  <span class="entry-mark" aria-hidden="true"></span>
@@ -72,7 +72,7 @@
72
72
 
73
73
  <% if own.owner? %>
74
74
  <p class="item-meta">
75
- <%= account_text("organizations.people", count: joined) %>
75
+ <%= account_text("organizations.member_count", count: joined) %>
76
76
  · <%= account_text("organizations.owners", count: owners) %>
77
77
  <% if waiting.positive? %>
78
78
  · <%= account_text("organizations.waiting_count", count: waiting) %>
@@ -81,8 +81,8 @@
81
81
 
82
82
  <div class="items">
83
83
  <% members.each do |member| %>
84
- <% person = member.actor %>
85
- <% yourself = person.id == own.actor_id %>
84
+ <% actor = member.actor %>
85
+ <% yourself = actor.id == own.actor_id %>
86
86
  <% label = Masks::Server::Members.label(member) || account_text("organizations.invited_by_manager") %>
87
87
  <div class="item item-member">
88
88
  <span class="item-name">
@@ -111,7 +111,7 @@
111
111
  class: "control control-sm", id: "role-#{organization.key}-#{member.id}",
112
112
  disabled: yourself && alone,
113
113
  data: { autosubmit: true },
114
- aria: { label: account_text("organizations.role_of", person: label) } %>
114
+ aria: { label: account_text("organizations.role_of", name: label) } %>
115
115
  <button type="submit" class="action action-quiet action-sm" data-autosubmitted <%= "disabled" if yourself && alone %>>
116
116
  <%= account_text("organizations.set_role") %>
117
117
  </button>
@@ -0,0 +1,27 @@
1
+ <% sign_out = local_assigns.fetch(:sign_out, false) %>
2
+ <% details = actor.display_details %>
3
+
4
+ <div class="actor">
5
+ <%= avatar_tag actor, size: 44, class: "actor-avatar" %>
6
+
7
+ <div class="actor-who">
8
+ <span class="actor-name"><%= actor.display_name %></span>
9
+
10
+ <% if details.any? || actor.email_unconfirmed? %>
11
+ <span class="actor-sub">
12
+ <%= details.join(" · ") %>
13
+ <% if actor.email_unconfirmed? %>
14
+ <span class="actor-note"><%= local_assigns[:unconfirmed] || t("application.actor.unconfirmed") %></span>
15
+ <% end %>
16
+ </span>
17
+ <% end %>
18
+
19
+ <% if actor.manages? %>
20
+ <span class="actor-role"><%= t("application.actor.manager") %></span>
21
+ <% end %>
22
+ </div>
23
+
24
+ <% if sign_out %>
25
+ <%= button_to t("application.actor.sign_out"), logout_path, class: "textlink", form_class: "signout" %>
26
+ <% end %>
27
+ </div>
@@ -6,7 +6,7 @@
6
6
  <h1 class="prompt-title"><%= t(".title") %></h1>
7
7
 
8
8
  <% if current_actor %>
9
- <%= render "masks/server/application/person", actor: current_actor, sign_out: true %>
9
+ <%= render "masks/server/application/actor", actor: current_actor, sign_out: true %>
10
10
  <% end %>
11
11
 
12
12
  <p class="said"><%= @description %></p>
@@ -43,7 +43,7 @@
43
43
 
44
44
  <div class="handshake-as">
45
45
  <span class="handshake-caption"><%= t(".signed_in_as") %></span>
46
- <%= render "masks/server/application/person", actor: current_actor, sign_out: true, unconfirmed: t(".unconfirmed") %>
46
+ <%= render "masks/server/application/actor", actor: current_actor, sign_out: true, unconfirmed: t(".unconfirmed") %>
47
47
  </div>
48
48
 
49
49
  <div class="slab handshake-plain">
@@ -4,7 +4,7 @@
4
4
  title: client ? t(".title_to", client: client) : t(".title"),
5
5
  lede: t(".lede") %>
6
6
 
7
- <%= render "masks/server/application/person", actor: login.actor, sign_out: true if login.actor %>
7
+ <%= render "masks/server/application/actor", actor: login.actor, sign_out: true if login.actor %>
8
8
 
9
9
  <div class="providers">
10
10
  <% (login.as_json["organizations"] || []).each do |organization| %>
@@ -14,7 +14,7 @@
14
14
 
15
15
  <%= render "masks/server/logins/head", title: t(".title", client: login.client&.name) %>
16
16
 
17
- <%= render "masks/server/application/person", actor: login.actor, sign_out: true if login.actor %>
17
+ <%= render "masks/server/application/actor", actor: login.actor, sign_out: true if login.actor %>
18
18
 
19
19
  <div class="slab">
20
20
  <div class="ledger-row">
@@ -12,7 +12,7 @@
12
12
  </p>
13
13
  </div>
14
14
 
15
- <%= render "masks/server/application/person", actor: current_actor %>
15
+ <%= render "masks/server/application/actor", actor: current_actor %>
16
16
 
17
17
  <p class="aside"><%= t(".scope") %></p>
18
18
 
@@ -155,7 +155,7 @@ de:
155
155
  lede_owner: "Als Eigentümer entscheiden Sie, wer dazugehört und welche Rolle jede Person hat. Apps, die Ihre Organisation anfordern, sehen die Rolle."
156
156
  lede_member: "Apps, die Ihre Organisation anfordern, sehen, dass Sie dazugehören, und Ihre Rolle."
157
157
  members: "Mitglieder"
158
- people:
158
+ member_count:
159
159
  one: "1 Mitglied"
160
160
  other: "%{count} Mitglieder"
161
161
  owners:
@@ -182,7 +182,7 @@ de:
182
182
  decline: "Ablehnen"
183
183
  confirm_decline: "Die Einladung zu %{organization} ablehnen? Ein Eigentümer muss Sie erneut einladen."
184
184
  set_role: "Rolle festlegen"
185
- role_of: "Rolle von %{person}"
185
+ role_of: "Rolle von %{name}"
186
186
  remove: "Entfernen"
187
187
  withdraw: "Zurückziehen"
188
188
  resend: "Erneut senden"
@@ -1,6 +1,6 @@
1
1
  de:
2
2
  application:
3
- person:
3
+ actor:
4
4
  manager: "Verwalter"
5
5
  unconfirmed: "unbestätigt"
6
6
  sign_out: "Abmelden"