masks-server 0.9.0 → 0.10.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/masks/server/client_authentication.rb +3 -2
  3. data/app/controllers/masks/server/addresses_controller.rb +6 -1
  4. data/app/controllers/masks/server/application_controller.rb +29 -1
  5. data/app/controllers/masks/server/authorize_controller.rb +23 -18
  6. data/app/controllers/masks/server/device_verifications_controller.rb +1 -1
  7. data/app/controllers/masks/server/logins_controller.rb +2 -1
  8. data/app/controllers/masks/server/notifications_controller.rb +11 -1
  9. data/app/controllers/masks/server/profiles_controller.rb +5 -0
  10. data/app/controllers/masks/server/saml_controller.rb +1 -7
  11. data/app/controllers/masks/server/tokens_controller.rb +8 -0
  12. data/app/graphql/masks/server/manage/mutations/create_actor.rb +1 -0
  13. data/app/graphql/masks/server/manage/mutations/update_actor.rb +1 -0
  14. data/app/graphql/masks/server/manage/mutations/update_client.rb +4 -0
  15. data/app/graphql/masks/server/manage/types/client_type.rb +2 -0
  16. data/app/graphql/masks/server/manage/types/query_type.rb +7 -0
  17. data/app/jobs/masks/server/cleanup_job.rb +1 -0
  18. data/app/ldap/masks/server/ldap/listener.rb +16 -3
  19. data/app/logins/masks/server/login_states/confirmation.rb +9 -3
  20. data/app/logins/masks/server/login_states/signup.rb +1 -0
  21. data/app/models/masks/server/actor.rb +25 -2
  22. data/app/models/masks/server/client.rb +15 -0
  23. data/app/models/masks/server/client_keys.rb +12 -1
  24. data/app/models/masks/server/declared.rb +2 -1
  25. data/app/models/masks/server/event.rb +1 -0
  26. data/app/models/masks/server/notifications.rb +8 -0
  27. data/app/models/masks/server/pending_request.rb +8 -0
  28. data/app/models/masks/server/refresh_token.rb +8 -0
  29. data/app/models/masks/server/released_nickname.rb +26 -0
  30. data/app/models/masks/server/scim/filter.rb +45 -5
  31. data/app/models/masks/server/scim/user.rb +1 -0
  32. data/app/models/masks/server/session.rb +3 -4
  33. data/app/views/masks/server/account/_info.html.erb +12 -4
  34. data/app/views/masks/server/account/index.html.erb +1 -1
  35. data/app/views/masks/server/authorize/error.html.erb +2 -2
  36. data/config/locales/de/account.yml +4 -0
  37. data/config/locales/de/actors.yml +2 -0
  38. data/config/locales/de/authorize.yml +2 -0
  39. data/config/locales/de/events.yml +2 -0
  40. data/config/locales/de/flashes.yml +1 -0
  41. data/config/locales/de/logins.yml +1 -0
  42. data/config/locales/de/manage/client.yml +3 -0
  43. data/config/locales/de/manage/ldap.yml +1 -0
  44. data/config/locales/de/notifications.yml +1 -0
  45. data/config/locales/en/account.yml +4 -0
  46. data/config/locales/en/actors.yml +2 -0
  47. data/config/locales/en/authorize.yml +2 -0
  48. data/config/locales/en/events.yml +2 -0
  49. data/config/locales/en/flashes.yml +1 -0
  50. data/config/locales/en/logins.yml +1 -0
  51. data/config/locales/en/manage/client.yml +3 -0
  52. data/config/locales/en/manage/ldap.yml +1 -0
  53. data/config/locales/en/notifications.yml +1 -0
  54. data/config/locales/es/account.yml +4 -0
  55. data/config/locales/es/actors.yml +2 -0
  56. data/config/locales/es/authorize.yml +2 -0
  57. data/config/locales/es/events.yml +2 -0
  58. data/config/locales/es/flashes.yml +1 -0
  59. data/config/locales/es/logins.yml +1 -0
  60. data/config/locales/es/manage/client.yml +3 -0
  61. data/config/locales/es/manage/ldap.yml +1 -0
  62. data/config/locales/es/notifications.yml +1 -0
  63. data/config/locales/fr/account.yml +4 -0
  64. data/config/locales/fr/actors.yml +2 -0
  65. data/config/locales/fr/authorize.yml +2 -0
  66. data/config/locales/fr/events.yml +2 -0
  67. data/config/locales/fr/flashes.yml +1 -0
  68. data/config/locales/fr/logins.yml +1 -0
  69. data/config/locales/fr/manage/client.yml +3 -0
  70. data/config/locales/fr/manage/ldap.yml +1 -0
  71. data/config/locales/fr/notifications.yml +1 -0
  72. data/config/routes.rb +1 -0
  73. data/db/migrate/20261018100000_create_released_nicknames.rb +22 -0
  74. data/db/migrate/20261018110000_add_refresh_token_absolute_lifetime_to_clients.rb +5 -0
  75. data/lib/masks/server/configuration.rb +8 -0
  76. data/lib/masks/server/tenancy/middleware.rb +9 -1
  77. data/public/masks-assets/.vite/manifest.json +2 -2
  78. data/public/masks-assets/assets/{app-B27oPUll.css → app-D7W0gAyT.css} +1 -1
  79. data/public/masks-assets/assets/manage-DR9qQiBU.js +569 -0
  80. data/public/masks-assets/assets/manage-DR9qQiBU.js.map +1 -0
  81. data/version.txt +1 -1
  82. metadata +7 -4
  83. data/public/masks-assets/assets/manage-Dbfiy3Mj.js +0 -567
  84. data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36e33f34e755bbbb4470a5c55b920624f2042678b87c810ce614e86f9638a4df
4
- data.tar.gz: 2bf178807ba578015922b44145ad0eb28454698adb877ac51b5059be57a35ca3
3
+ metadata.gz: 544dc01d0600507eb713667f0d8cf57275b4b7d957b603e3085f9e197e7e63c6
4
+ data.tar.gz: 70f664c959b40e98e9a273582a45c0dba352e5186430a72c26e3a8dd87db978b
5
5
  SHA512:
6
- metadata.gz: 3d32dd6592c56c0132971c1f322ca90b2a31df6832c0b9fe33d5c41c7d5d7aab443b4c6b2fec0a9d3ac0635b22b6d86bbda1c451bd791fd3794e4776c1edd1c8
7
- data.tar.gz: 59f5658c277590947cf74f3b8e57fe2d9d7b01f1a160bdc3bdbc3593f4aba9b9395b08255b866e54029f48aff6d9b7b1384daf9ab1b21b95f7468678a800fbcb
6
+ metadata.gz: a5c6177ca713658c3003f33cea0b8902c6810266271f4d4e6d0215d8a291b6da13a47add90dca3b65df3f9614f6161dd5627853c3e9038ddafffcc0963ce27d4
7
+ data.tar.gz: 872c0d7ce2bd4090f885fdbce3eb82d8459ff73840ac89d3bd9cd2088ae334dc1efb2c34fe7bc17a2f413492d07c15c74a69f73b771f98847a4a69ae10416228
@@ -16,9 +16,10 @@ module Masks
16
16
  client_refused!("no client is registered with that client_id") if client.nil?
17
17
  client_refused!("this client authenticates with a signed assertion, not a secret") if client.asserts?
18
18
 
19
- client_refused! unless client.authenticate_secret(secret)
19
+ return client if client.authenticate_secret(secret)
20
20
 
21
- client
21
+ client_refused!("this client's secret expired on #{client.secret_expires_at.iso8601}, and a manager rotates it") if client.secret_expired? && client.secret_matches?(secret)
22
+ client_refused!
22
23
  end
23
24
 
24
25
  def asserted_client(assertion)
@@ -8,6 +8,7 @@ module Masks
8
8
  only: :update
9
9
 
10
10
  before_action :require_actor
11
+ before_action :known_channel
11
12
  before_action -> { reauthenticated!(anchor, t("addresses.again")) }
12
13
  before_action :refuse_directed
13
14
 
@@ -55,7 +56,11 @@ module Masks
55
56
  private
56
57
 
57
58
  def channel
58
- params[:channel].to_s
59
+ @channel ||= Addresses::CHANNELS.keys.find { |known| known == params[:channel].to_s }
60
+ end
61
+
62
+ def known_channel
63
+ head :not_found if channel.nil?
59
64
  end
60
65
 
61
66
  def anchor
@@ -264,6 +264,34 @@ module Masks
264
264
  )
265
265
  end
266
266
 
267
+ def answerable?(pending)
268
+ authorization = pending.authorization
269
+
270
+ authorization.client&.approved? || ours?(authorization.redirect_uri)
271
+ end
272
+
273
+ def refuse_on_masks(pending, refusal)
274
+ pending.refuse!(refusal.error, refusal.description)
275
+ PendingRequest.claim(rid_for(pending))
276
+
277
+ authorize_refused_path(rid: rid_for(pending))
278
+ end
279
+
280
+ def refused_request(rid)
281
+ return nil if rid.blank? || !tracked(REQUESTS).value?(rid)
282
+
283
+ PendingRequest.spent(rid)&.then { |held| held if held.consumed? }
284
+ end
285
+
286
+ def ours?(target)
287
+ here = URI.parse(issuer.url)
288
+ there = URI.parse(target.to_s)
289
+
290
+ there.scheme == here.scheme && there.host == here.host && there.port == here.port
291
+ rescue URI::InvalidURIError
292
+ false
293
+ end
294
+
267
295
  def authorize_url_for(pending)
268
296
  "#{authorize_path}?#{URI.encode_www_form(pending.authorization.query_pairs)}"
269
297
  end
@@ -303,7 +331,7 @@ module Masks
303
331
 
304
332
  def settle!(login)
305
333
  hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
306
- authenticated_at: login.factored_at(:first_factor))
334
+ authenticated_at: login.authenticated_at)
307
335
  forget_login
308
336
  end
309
337
 
@@ -45,6 +45,7 @@ module Masks
45
45
 
46
46
  login = advance(pending)
47
47
 
48
+ return redirect_to(refuse_on_masks(pending, login.refusal)) if login.refused? && !answerable?(pending)
48
49
  return answer(deny(pending, login.refusal.error, login.refusal.description)) if login.refused?
49
50
  return interaction_required(pending) if login.prompted? && pending.silent?
50
51
  return redirect_to(login.redirect_to, allow_other_host: true) if login.redirect_to.present?
@@ -53,6 +54,15 @@ module Masks
53
54
  complete(pending, attempt, login)
54
55
  end
55
56
 
57
+ def refused
58
+ refusal = refused_request(params[:rid])&.refusal
59
+
60
+ return show_error("invalid_request", "that sign-in request has expired", :bad_request) if refusal.nil?
61
+
62
+ @refused = true
63
+ show_error(refusal["error"], refusal["description"], :forbidden)
64
+ end
65
+
56
66
  private
57
67
 
58
68
  def validate(authorization)
@@ -66,6 +76,7 @@ module Masks
66
76
  req.verify_redirect_uri!(client.redirect_uri?(req.redirect_uri) ? [ req.redirect_uri.to_s ] : client.redirect_uris)
67
77
  req.verified_redirect_uri = with_issuer(req.verified_redirect_uri)
68
78
  res.redirect_uri = req.verified_redirect_uri
79
+ req.verified_redirect_uri = nil unless hears_errors?(client, req.redirect_uri)
69
80
 
70
81
  permit(req) { authorization.validate! }
71
82
  end
@@ -97,7 +108,7 @@ module Masks
97
108
  actor: current_actor,
98
109
  device: current_device,
99
110
  session: current_session,
100
- authenticated_at: login.authenticated_at || current_session&.authenticated_at,
111
+ authenticated_at: current_session&.authenticated_at,
101
112
  amr: login.amr.presence || current_session&.amr,
102
113
  organization: login.state("organization-choice").selected
103
114
  )
@@ -121,21 +132,20 @@ module Masks
121
132
  client = Client.authenticating(params[:client_id])
122
133
  target = params[:redirect_uri].to_s
123
134
 
124
- if target.present? && (client&.redirect_uri?(target) || ours?(target))
135
+ if target.present? && hears_errors?(client, target)
125
136
  return redirect_to refusal_uri(target, error), allow_other_host: true
126
137
  end
127
138
 
128
- @error_code = error.error.to_s
129
- @error_description = error.description
139
+ show_error(error.error.to_s, error.description, error.status)
140
+ end
141
+
142
+ def show_error(code, description, status)
143
+ @error_code = code
144
+ @error_description = description
130
145
 
131
146
  respond_to do |format|
132
- format.html { render :error, status: error.status }
133
- format.any do
134
- render json: {
135
- "error" => @error_code,
136
- "error_description" => @error_description
137
- }, status: error.status
138
- end
147
+ format.html { render :error, status: status }
148
+ format.any { render json: { "error" => code, "error_description" => description }, status: status }
139
149
  end
140
150
  end
141
151
 
@@ -163,13 +173,8 @@ module Masks
163
173
  Rack::OAuth2::Server::Authorize::BadRequest.new(error, description)
164
174
  end
165
175
 
166
- def ours?(target)
167
- here = URI.parse(issuer.url)
168
- there = URI.parse(target.to_s)
169
-
170
- there.scheme == here.scheme && there.host == here.host && there.port == here.port
171
- rescue URI::InvalidURIError
172
- false
176
+ def hears_errors?(client, target)
177
+ ours?(target) || (client.present? && client.approved? && client.redirect_uri?(target))
173
178
  end
174
179
 
175
180
  def refusal_uri(target, error)
@@ -59,7 +59,7 @@ module Masks
59
59
  actor: current_actor,
60
60
  session: current_session,
61
61
  device: current_device,
62
- authenticated_at: login.authenticated_at || current_session&.authenticated_at,
62
+ authenticated_at: current_session&.authenticated_at,
63
63
  amr: login.amr.presence || current_session&.amr,
64
64
  organization: login.state("organization-choice").selected
65
65
  )
@@ -152,7 +152,7 @@ module Masks
152
152
 
153
153
  if login.touched?(:first_factor)
154
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
+ authenticated_at: login.authenticated_at)
156
156
  elsif login.signed_in? && login.touched?(:second_factor)
157
157
  current_session.update!(amr: Array(current_session.amr) | login.amr)
158
158
  end
@@ -191,6 +191,7 @@ module Masks
191
191
  return refuse_device(pending) if login.refused? && pending&.device?
192
192
  return refuse_saml(pending) if login.refused? && pending&.saml?
193
193
  return saml_resume_path(rid: rid_for(pending)) if pending&.saml?
194
+ return refuse_on_masks(pending, login.refusal) if login.refused? && pending && !answerable?(pending)
194
195
  return deny(pending, login.refusal.error, login.refusal.description) if login.refused? && pending
195
196
  return device_url_for(pending) if pending&.device?
196
197
  return authorize_url_for(pending) if pending
@@ -2,9 +2,19 @@ module Masks
2
2
  module Server
3
3
  class NotificationsController < ApplicationController
4
4
  before_action :require_actor
5
+ before_action -> { reauthenticated!("notifications", t("notifications.again")) }
5
6
 
6
7
  def update
8
+ was = current_actor.muted_notifications
7
9
  current_actor.update!(notifications: wanted)
10
+ now = current_actor.muted_notifications
11
+
12
+ muted = now - was
13
+ unmuted = was - now
14
+
15
+ if muted.any? || unmuted.any?
16
+ Event.record!(Event::NOTIFICATIONS_CHANGED, actor: current_actor, muted: muted, unmuted: unmuted)
17
+ end
8
18
 
9
19
  redirect_to "#{root_path}#notifications", notice: t("notifications.saved")
10
20
  end
@@ -12,7 +22,7 @@ module Masks
12
22
  private
13
23
 
14
24
  def wanted
15
- Array(params[:notifications]).map(&:to_s) & Notifications::MAILED
25
+ Array(params[:notifications]).map(&:to_s) & Notifications::MUTABLE
16
26
  end
17
27
 
18
28
  def require_actor
@@ -5,6 +5,7 @@ module Masks
5
5
 
6
6
  def update
7
7
  return refuse(t("profiles.directed")) if current_actor.directed?
8
+ return unless kept_nickname? || reauthenticated!("profile", t("profiles.again"))
8
9
 
9
10
  current_actor.assign_attributes(profile)
10
11
  changed = current_actor.changed
@@ -22,6 +23,10 @@ module Masks
22
23
  params.permit(:name, :nickname).to_h.transform_values { |value| value.strip.presence }
23
24
  end
24
25
 
26
+ def kept_nickname?
27
+ !profile.key?("nickname") || profile["nickname"] == current_actor.nickname
28
+ end
29
+
25
30
  def require_actor
26
31
  redirect_to login_path unless current_actor
27
32
  end
@@ -139,7 +139,7 @@ module Masks
139
139
  assertion = response_for(pending)
140
140
  encoded = assertion.success(
141
141
  actor: current_actor, session: current_session, name_id_format: saml["name_id_format"], scopes: scopes,
142
- authenticated_at: login.authenticated_at || current_session&.authenticated_at,
142
+ authenticated_at: current_session&.authenticated_at,
143
143
  amr: login.amr.presence || current_session&.amr
144
144
  )
145
145
 
@@ -174,12 +174,6 @@ module Masks
174
174
  )
175
175
  end
176
176
 
177
- def refused_request(rid)
178
- return nil if rid.blank? || !tracked(REQUESTS).value?(rid)
179
-
180
- PendingRequest.spent(rid)&.then { |held| held if held.consumed? }
181
- end
182
-
183
177
  def post(pending, encoded)
184
178
  post_saml(pending.client, pending.redirect_uri, "SAMLResponse", encoded, relay_state: pending.held("saml")["relay_state"])
185
179
  end
@@ -124,6 +124,7 @@ module Masks
124
124
 
125
125
  req.invalid_grant!("that refresh token was issued to another client") if token.client_id != client.id
126
126
  req.invalid_grant!("the session that refresh token was issued in has ended") if outlived?(token)
127
+ exhausted!(req, token, client)
127
128
  member!(req, token)
128
129
  consented!(req, token, client)
129
130
  Current.organization = token.organization
@@ -180,6 +181,13 @@ module Masks
180
181
  held.present? && held.bounded? && !held.live?
181
182
  end
182
183
 
184
+ def exhausted!(req, token, client)
185
+ return unless token.exhausted?(client.refresh_absolute_lifetime)
186
+
187
+ token.revoke_family!
188
+ req.invalid_grant!("the sign-in behind that refresh token is older than #{client.name} allows")
189
+ end
190
+
183
191
  def replayed!(spent, client)
184
192
  return if spent.nil? || !spent.consumed?
185
193
 
@@ -18,6 +18,7 @@ module Masks
18
18
  granting!(scopes)
19
19
 
20
20
  actor = Actor.new(
21
+ trusted_rename: true,
21
22
  nickname: nickname,
22
23
  email: email,
23
24
  scopes: Scopes.join(Scopes.list(scopes).presence || Scopes::STANDARD)
@@ -29,6 +29,7 @@ module Masks
29
29
  needs!(:security, "changing someone's email address") if changing_email
30
30
  previous = actor.email if changing_email && actor.email_verified_at.present?
31
31
 
32
+ actor.trusted_rename = true
32
33
  actor.assign_attributes(attributes)
33
34
  actor.email_verified_at = nil if changing_email
34
35
  actor.phone_verified_at = nil if attributes.key?(:phone) && actor.phone_changed?
@@ -36,6 +36,9 @@ module Masks
36
36
  description: "Seconds an access token lasts, from 60 to a day. Zero goes back to an hour."
37
37
  argument :refresh_token_lifetime, Integer, required: false,
38
38
  description: "Seconds a refresh token lasts unused, from 300 to 400 days. Zero goes back to 30 days."
39
+ argument :refresh_token_absolute_lifetime, Integer, required: false,
40
+ description: "Seconds from a sign-in until its refresh tokens stop, however often they are used, " \
41
+ "from 300 to 400 days. Zero goes back to 90 days."
39
42
  argument :allowed_groups, [ String ], required: false,
40
43
  description: "Group keys, or organization roles as key:role, whose people may sign in to this client. Empty lets everyone in."
41
44
  argument :sign_in_policy, ID, required: false
@@ -68,6 +71,7 @@ module Masks
68
71
  attributes[:consent_lifetime] = nil if attributes[:consent_lifetime]&.zero?
69
72
  attributes[:access_token_lifetime] = nil if attributes[:access_token_lifetime]&.zero?
70
73
  attributes[:refresh_token_lifetime] = nil if attributes[:refresh_token_lifetime]&.zero?
74
+ attributes[:refresh_token_absolute_lifetime] = nil if attributes[:refresh_token_absolute_lifetime]&.zero?
71
75
  client.assign_attributes(attributes)
72
76
  client.response_types = client.default_response_types if attributes.key?(:grant_types)
73
77
  client.required_scopes = Scopes.join(required_scopes) unless required_scopes.nil?
@@ -16,6 +16,8 @@ module Masks
16
16
  description: "Seconds an access token lasts. Null is an hour."
17
17
  field :refresh_token_lifetime, Integer,
18
18
  description: "Seconds a refresh token lasts unused. Each refresh starts it over. Null is 30 days."
19
+ field :refresh_token_absolute_lifetime, Integer,
20
+ description: "Seconds from a sign-in until its refresh tokens stop, however often they are used. Null is 90 days."
19
21
  field :allowed_groups, [ String ], null: false,
20
22
  description: "Group keys, or organization roles as key:role, whose people may sign in. Empty lets everyone in."
21
23
  field :namespaces, [ "Masks::Server::Manage::Types::NamespaceType" ], null: false
@@ -139,6 +139,9 @@ module Masks
139
139
  field :ldap_base_dn, String, null: false, description: "The base DN of this tenant's LDAP directory."
140
140
  field :ldap_port, Integer, null: false, description: "The port masks serves LDAP on."
141
141
  field :ldap_tls, Boolean, null: false, description: "Whether that port speaks LDAPS."
142
+ field :ldap_local_only, Boolean, null: false,
143
+ description: "Whether the directory answers only on the server's own machine, " \
144
+ "since it has no certificate and MASKS_LDAP_PLAINTEXT is not set."
142
145
  field :saml_metadata_url, String, null: false
143
146
 
144
147
  field :minimum_password, Integer, null: false
@@ -236,6 +239,10 @@ module Masks
236
239
  ::Rails.configuration.masks.ldap_cert.present?
237
240
  end
238
241
 
242
+ def ldap_local_only
243
+ Ldap::Listener.local_only?
244
+ end
245
+
239
246
  def ldap_base_dn
240
247
  Ldap::Directory.base_for(Current.tenant).to_s
241
248
  end
@@ -33,6 +33,7 @@ module Masks
33
33
  Event.where(created_at: ...tenant.event_retention.ago).in_batches(of: BATCH).delete_all
34
34
  Members.purge_lapsed!
35
35
  Replay.where(expires_at: ...Time.current).in_batches(of: BATCH).delete_all
36
+ ReleasedNickname.lapsed.in_batches(of: BATCH).delete_all
36
37
  end
37
38
 
38
39
  def sweep_tokens(cutoff)
@@ -6,11 +6,24 @@ module Masks
6
6
  IDLE = 5.minutes
7
7
  HANDSHAKE = 10.seconds
8
8
 
9
- attr_reader :port
9
+ EVERY_ADDRESS = "0.0.0.0".freeze
10
+ THIS_MACHINE = "127.0.0.1".freeze
11
+
12
+ attr_reader :host, :port
10
13
 
11
14
  class << self
12
15
  def from_configuration(config = ::Rails.configuration.masks)
13
- new(host: "0.0.0.0", port: config.ldap_port, tls: tls(config.ldap_cert, config.ldap_key))
16
+ tls = tls(config.ldap_cert, config.ldap_key)
17
+
18
+ new(host: host_for(tls: tls, plaintext: config.ldap_plaintext), port: config.ldap_port, tls: tls)
19
+ end
20
+
21
+ def host_for(tls:, plaintext:)
22
+ tls || plaintext ? EVERY_ADDRESS : THIS_MACHINE
23
+ end
24
+
25
+ def local_only?(config = ::Rails.configuration.masks)
26
+ config.ldap_cert.blank? && !config.ldap_plaintext
14
27
  end
15
28
 
16
29
  def tls(cert, key)
@@ -40,7 +53,7 @@ module Masks
40
53
  def start
41
54
  @server = TCPServer.new(@host, @wanted_port)
42
55
  @port = @server.local_address.ip_port
43
- @logger.info("masks ldap: listening on #{@host}:#{@port}#{' with TLS' if @tls}")
56
+ @logger.info("masks ldap: listening on #{@host}:#{@port}#{' with TLS' if @tls}#{' on this machine only' if @host == THIS_MACHINE}")
44
57
  @acceptor = Thread.new { accept_loop }
45
58
  self
46
59
  end
@@ -23,11 +23,11 @@ module Masks
23
23
  resend
24
24
  end
25
25
 
26
- handles "confirm:add-phone" do
26
+ handles "confirm:add-phone", limit: :sending do
27
27
  add_phone
28
28
  end
29
29
 
30
- handles "confirm:change-phone" do
30
+ handles "confirm:change-phone", limit: :sending do
31
31
  change_phone
32
32
  end
33
33
 
@@ -129,9 +129,15 @@ module Masks
129
129
  end
130
130
 
131
131
  def send_code(channel)
132
+ @tokens&.delete(channel)
133
+
134
+ if ConfirmationCode.crowded?(actor: actor, channel: channel)
135
+ login.store[HELD] = held.merge("#{channel}_sent" => Time.current.to_i)
136
+ return warn!(channel == ConfirmationCode::PHONE ? "too-many-texts" : "too-many-codes")
137
+ end
138
+
132
139
  token = Confirmations.send_code(actor, channel, journey: Masks::Server::Journey.sign_in(login))
133
140
 
134
- @tokens&.delete(channel)
135
141
  login.store[HELD] = held.merge(channel => token.id, "#{channel}_sent" => Time.current.to_i)
136
142
  end
137
143
 
@@ -280,6 +280,7 @@ module Masks
280
280
 
281
281
  now = Time.current
282
282
  actor = Masks::Server::Actor.new(
283
+ trusted_rename: @first_run,
283
284
  nickname: held["nickname"],
284
285
  email: held["email"],
285
286
  name: held["name"],
@@ -33,6 +33,9 @@ module Masks
33
33
  has_many :groups, through: :group_members
34
34
 
35
35
  before_destroy :sign_out_everywhere!, prepend: true
36
+ before_destroy :release_nickname
37
+
38
+ attr_accessor :trusted_rename
36
39
 
37
40
  validates :nickname,
38
41
  uniqueness: { scope: :tenant_id, case_sensitive: false },
@@ -52,8 +55,10 @@ module Masks
52
55
  allow_blank: true
53
56
 
54
57
  validate :named
58
+ validate :nickname_free, if: -> { nickname.present? && will_save_change_to_nickname? && !trusted_rename }
55
59
 
56
60
  before_save :activate_once_a_password_exists
61
+ after_save :release_previous_nickname, if: :saved_change_to_nickname?
57
62
  before_save :forget_code_factors_for_new_addresses
58
63
  after_update :forget_mailed_links, if: :saved_change_to_email?
59
64
 
@@ -282,11 +287,16 @@ module Masks
282
287
 
283
288
  held.find_each(&:revoke!)
284
289
  RefreshToken.where(actor_id: id).live.find_each(&:revoke!)
290
+
291
+ issued = AccessToken.where(actor_id: id).live
292
+ issued = issued.where.not(session_id: keeping.id) if keeping
293
+ issued.update_all(consumed_at: Time.current, updated_at: Time.current)
294
+
285
295
  DeviceFactor.forget!(actor: self)
286
296
  end
287
297
 
288
298
  def notified?(action)
289
- Notifications.mailed?(action) && !muted_notifications.include?(action.to_s)
299
+ Notifications.mailed?(action) && (Notifications.always?(action) || !muted_notifications.include?(action.to_s))
290
300
  end
291
301
 
292
302
  def notifications
@@ -294,7 +304,7 @@ module Masks
294
304
  end
295
305
 
296
306
  def notifications=(wanted)
297
- self.muted_notifications = Notifications::MAILED - Array(wanted).map(&:to_s)
307
+ self.muted_notifications = Notifications::MUTABLE - Array(wanted).map(&:to_s)
298
308
  end
299
309
 
300
310
  def otp?
@@ -508,6 +518,19 @@ module Masks
508
518
  end
509
519
  end
510
520
 
521
+ def nickname_free
522
+ errors.add(:nickname, :held) if ReleasedNickname.held_from?(nickname, self)
523
+ end
524
+
525
+ def release_previous_nickname
526
+ ReleasedNickname.where(nickname: nickname.to_s.downcase, actor_id: id).delete_all if nickname.present?
527
+ ReleasedNickname.release!(nickname_before_last_save, actor: self)
528
+ end
529
+
530
+ def release_nickname
531
+ ReleasedNickname.release!(nickname, actor: self)
532
+ end
533
+
511
534
  def manages_by_both
512
535
  errors.add(:nickname, :blank) if nickname.blank?
513
536
  errors.add(:email, :blank) if email.blank?
@@ -82,6 +82,8 @@ module Masks
82
82
  validate :resources_are_named_only_when_approved
83
83
  validates :access_token_lifetime, numericality: { only_integer: true, in: 60..1.day.to_i }, allow_nil: true
84
84
  validates :refresh_token_lifetime, numericality: { only_integer: true, in: 5.minutes.to_i..400.days.to_i }, allow_nil: true
85
+ validates :refresh_token_absolute_lifetime, numericality: { only_integer: true, in: 5.minutes.to_i..400.days.to_i },
86
+ allow_nil: true
85
87
  validates :consent_lifetime, numericality: { only_integer: true, greater_than_or_equal_to: 300,
86
88
  less_than_or_equal_to: Consent::LONGEST.to_i }, allow_nil: true
87
89
 
@@ -377,6 +379,11 @@ module Masks
377
379
 
378
380
  def authenticate_secret(candidate)
379
381
  return true if public?
382
+
383
+ secret_matches?(candidate) && !secret_expired?
384
+ end
385
+
386
+ def secret_matches?(candidate)
380
387
  return false unless secret?
381
388
  return false if secret_digest.blank? || candidate.blank?
382
389
 
@@ -385,6 +392,10 @@ module Masks
385
392
  false
386
393
  end
387
394
 
395
+ def secret_expired?
396
+ secret_expires_at.present? && secret_expires_at.past?
397
+ end
398
+
388
399
  def redirect_uri?(candidate)
389
400
  redirect_uris.include?(candidate.to_s) || redirect_uris.any? { |registered| same_loopback?(registered, candidate.to_s) }
390
401
  end
@@ -445,6 +456,10 @@ module Masks
445
456
  refresh_token_lifetime&.seconds || RefreshToken.lifetime
446
457
  end
447
458
 
459
+ def refresh_absolute_lifetime
460
+ refresh_token_absolute_lifetime&.seconds || RefreshToken.absolute_lifetime
461
+ end
462
+
448
463
  def admits?(actor)
449
464
  allowed_groups.empty? || actor.group_labels.intersect?(allowed_groups)
450
465
  end
@@ -78,7 +78,7 @@ module Masks
78
78
  raise Refused, "#{algorithm.presence || 'that'} is not an algorithm #{subject} may be signed with" unless ALGORITHMS.include?(algorithm)
79
79
 
80
80
  verified = attempt(token, header, algorithm, fresh: false)
81
- verified ||= attempt(token, header, algorithm, fresh: true) if remote? && unknown_kid?(header)
81
+ verified ||= attempt(token, header, algorithm, fresh: true) if remote? && unknown_kid?(header) && refetch_open?
82
82
 
83
83
  verified || raise(Refused, "#{subject} was not signed by a key this client registered")
84
84
  rescue JWT::DecodeError
@@ -127,9 +127,20 @@ module Masks
127
127
  kid ? held.select { |jwk| jwk["kid"] == kid } : held
128
128
  end
129
129
 
130
+ def refetch_open?
131
+ return false if ::Rails.cache.exist?(refetched_key)
132
+
133
+ ::Rails.cache.write(refetched_key, Time.current.to_i, expires_in: LIFETIME)
134
+ true
135
+ end
136
+
130
137
  def cache_key
131
138
  "client-jwks:#{client.tenant_id}:#{client.id}:#{Digest::SHA256.hexdigest(client.jwks_uri.to_s)}"
132
139
  end
140
+
141
+ def refetched_key
142
+ "#{cache_key}:refetched"
143
+ end
133
144
  end
134
145
  end
135
146
  end
@@ -5,7 +5,7 @@ module Masks
5
5
 
6
6
  TENANT_KEYS = %w[name accent_color groups apps].freeze
7
7
  APP_KEYS = %w[preset url slug name client_id client_secret_env redirect_uris scopes allowed_groups
8
- access_token_lifetime refresh_token_lifetime].freeze
8
+ access_token_lifetime refresh_token_lifetime refresh_token_absolute_lifetime].freeze
9
9
 
10
10
  def self.apply!(path = ::Rails.configuration.masks.config_path)
11
11
  return [] if path.blank?
@@ -92,6 +92,7 @@ module Masks
92
92
  allowed_groups: Array(declared["allowed_groups"]),
93
93
  access_token_lifetime: declared["access_token_lifetime"],
94
94
  refresh_token_lifetime: declared["refresh_token_lifetime"],
95
+ refresh_token_absolute_lifetime: declared["refresh_token_absolute_lifetime"],
95
96
  consent_required: false
96
97
  }
97
98
 
@@ -18,6 +18,7 @@ module Masks
18
18
 
19
19
  ACCOUNT_CREATED = "account.created".freeze
20
20
  ACCOUNT_EXPORTED = "account.exported".freeze
21
+ NOTIFICATIONS_CHANGED = "notifications.changed".freeze
21
22
  INVITATION_SENT = "invitation.sent".freeze
22
23
  INVITATION_ACCEPTED = "invitation.accepted".freeze
23
24
 
@@ -51,6 +51,10 @@ module Masks
51
51
 
52
52
  MAILED = GROUPS.values.flatten.freeze
53
53
 
54
+ ALWAYS = [ Event::PASSWORD_CHANGED, Event::PASSWORD_RESET_COMPLETED, Event::SIGN_IN_RISKY ].freeze
55
+
56
+ MUTABLE = (MAILED - ALWAYS).freeze
57
+
54
58
  ONCE_PER_DEVICE = [ Event::SESSION_STARTED ].freeze
55
59
 
56
60
  class << self
@@ -58,6 +62,10 @@ module Masks
58
62
  MAILED.include?(action.to_s)
59
63
  end
60
64
 
65
+ def always?(action)
66
+ ALWAYS.include?(action.to_s)
67
+ end
68
+
61
69
  def raised(event)
62
70
  return false unless mailed?(event.action) || WARNS_PREVIOUS.include?(event.action)
63
71
  return false unless event.actor_id && ActorMailer.deliverable?
@@ -39,6 +39,14 @@ module Masks
39
39
  held("max_age")
40
40
  end
41
41
 
42
+ def refuse!(error, description)
43
+ update!(payload: (payload || {}).merge("refusal" => { "error" => error, "description" => description }))
44
+ end
45
+
46
+ def refusal
47
+ held("refusal")
48
+ end
49
+
42
50
  def issue_code!(actor:, device: nil, session: nil, authenticated_at: nil, amr: nil, organization: nil)
43
51
  AuthorizationCode.mint!(
44
52
  organization: organization,