masks-server 0.8.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/masks/server/client_authentication.rb +4 -3
  3. data/app/controllers/concerns/masks/server/resource_token.rb +12 -3
  4. data/app/controllers/masks/server/addresses_controller.rb +6 -1
  5. data/app/controllers/masks/server/application_controller.rb +36 -6
  6. data/app/controllers/masks/server/authorize_controller.rb +24 -19
  7. data/app/controllers/masks/server/device_verifications_controller.rb +1 -1
  8. data/app/controllers/masks/server/logins_controller.rb +3 -1
  9. data/app/controllers/masks/server/notifications_controller.rb +11 -1
  10. data/app/controllers/masks/server/profiles_controller.rb +5 -0
  11. data/app/controllers/masks/server/registrations_controller.rb +9 -1
  12. data/app/controllers/masks/server/saml_controller.rb +1 -7
  13. data/app/controllers/masks/server/scim/metadata_controller.rb +1 -1
  14. data/app/controllers/masks/server/tokens_controller.rb +9 -1
  15. data/app/graphql/masks/server/manage/mutations/add_app.rb +1 -1
  16. data/app/graphql/masks/server/manage/mutations/create_actor.rb +1 -0
  17. data/app/graphql/masks/server/manage/mutations/update_actor.rb +1 -0
  18. data/app/graphql/masks/server/manage/mutations/update_client.rb +4 -0
  19. data/app/graphql/masks/server/manage/types/client_type.rb +2 -0
  20. data/app/graphql/masks/server/manage/types/query_type.rb +7 -0
  21. data/app/helpers/masks/server/account_helper.rb +1 -1
  22. data/app/jobs/masks/server/cleanup_job.rb +1 -0
  23. data/app/ldap/masks/server/ldap/listener.rb +16 -3
  24. data/app/logins/masks/server/login_states/confirmation.rb +9 -3
  25. data/app/logins/masks/server/login_states/signup.rb +1 -0
  26. data/app/models/masks/server/actor.rb +28 -5
  27. data/app/models/masks/server/client.rb +61 -6
  28. data/app/models/masks/server/client_keys.rb +12 -1
  29. data/app/models/masks/server/declared.rb +2 -1
  30. data/app/models/masks/server/event.rb +1 -0
  31. data/app/models/masks/server/handshake.rb +12 -6
  32. data/app/models/masks/server/login.rb +3 -6
  33. data/app/models/masks/server/notifications.rb +8 -0
  34. data/app/models/masks/server/pending_handshake.rb +2 -1
  35. data/app/models/masks/server/pending_request.rb +8 -0
  36. data/app/models/masks/server/refresh_token.rb +8 -0
  37. data/app/models/masks/server/released_nickname.rb +26 -0
  38. data/app/models/masks/server/scim/filter.rb +45 -5
  39. data/app/models/masks/server/scim/user.rb +1 -0
  40. data/app/models/masks/server/session.rb +6 -5
  41. data/app/policies/masks/server/exchange_policy.rb +1 -1
  42. data/app/policies/masks/server/grant_checks.rb +1 -1
  43. data/app/views/masks/server/account/_info.html.erb +13 -5
  44. data/app/views/masks/server/account/_organizations.html.erb +4 -4
  45. data/app/views/masks/server/account/index.html.erb +1 -1
  46. data/app/views/masks/server/application/_actor.html.erb +27 -0
  47. data/app/views/masks/server/authorize/error.html.erb +2 -2
  48. data/app/views/masks/server/handshakes/refused.html.erb +1 -1
  49. data/app/views/masks/server/handshakes/show.html.erb +1 -1
  50. data/app/views/masks/server/logins/_choose_organization.html.erb +1 -1
  51. data/app/views/masks/server/logins/_consent.html.erb +1 -1
  52. data/app/views/masks/server/sessions/confirm.html.erb +1 -1
  53. data/config/locales/de/account.yml +6 -2
  54. data/config/locales/de/actors.yml +2 -0
  55. data/config/locales/de/application.yml +1 -1
  56. data/config/locales/de/authorize.yml +2 -0
  57. data/config/locales/de/events.yml +7 -5
  58. data/config/locales/de/flashes.yml +1 -0
  59. data/config/locales/de/logins.yml +1 -0
  60. data/config/locales/de/manage/client.yml +3 -0
  61. data/config/locales/de/manage/groups.yml +1 -1
  62. data/config/locales/de/manage/ldap.yml +1 -0
  63. data/config/locales/de/manage/organization.yml +1 -1
  64. data/config/locales/de/notifications.yml +1 -0
  65. data/config/locales/en/account.yml +6 -2
  66. data/config/locales/en/actors.yml +2 -0
  67. data/config/locales/en/application.yml +1 -1
  68. data/config/locales/en/authorize.yml +2 -0
  69. data/config/locales/en/events.yml +7 -5
  70. data/config/locales/en/flashes.yml +1 -0
  71. data/config/locales/en/logins.yml +1 -0
  72. data/config/locales/en/manage/client.yml +3 -0
  73. data/config/locales/en/manage/groups.yml +1 -1
  74. data/config/locales/en/manage/ldap.yml +1 -0
  75. data/config/locales/en/manage/organization.yml +1 -1
  76. data/config/locales/en/notifications.yml +1 -0
  77. data/config/locales/es/account.yml +6 -2
  78. data/config/locales/es/actors.yml +2 -0
  79. data/config/locales/es/application.yml +1 -1
  80. data/config/locales/es/authorize.yml +2 -0
  81. data/config/locales/es/events.yml +7 -5
  82. data/config/locales/es/flashes.yml +1 -0
  83. data/config/locales/es/logins.yml +1 -0
  84. data/config/locales/es/manage/client.yml +3 -0
  85. data/config/locales/es/manage/groups.yml +1 -1
  86. data/config/locales/es/manage/ldap.yml +1 -0
  87. data/config/locales/es/manage/organization.yml +1 -1
  88. data/config/locales/es/notifications.yml +1 -0
  89. data/config/locales/fr/account.yml +6 -2
  90. data/config/locales/fr/actors.yml +2 -0
  91. data/config/locales/fr/application.yml +1 -1
  92. data/config/locales/fr/authorize.yml +2 -0
  93. data/config/locales/fr/events.yml +7 -5
  94. data/config/locales/fr/flashes.yml +1 -0
  95. data/config/locales/fr/logins.yml +1 -0
  96. data/config/locales/fr/manage/client.yml +3 -0
  97. data/config/locales/fr/manage/groups.yml +1 -1
  98. data/config/locales/fr/manage/ldap.yml +1 -0
  99. data/config/locales/fr/manage/organization.yml +1 -1
  100. data/config/locales/fr/notifications.yml +1 -0
  101. data/config/routes.rb +1 -0
  102. data/db/migrate/20261013010000_add_handshake_return_to_to_clients.rb +5 -0
  103. data/db/migrate/20261018100000_create_released_nicknames.rb +22 -0
  104. data/db/migrate/20261018110000_add_refresh_token_absolute_lifetime_to_clients.rb +5 -0
  105. data/lib/generators/masks/server/install/install_generator.rb +2 -2
  106. data/lib/masks/server/browser_access.rb +35 -0
  107. data/lib/masks/server/configuration.rb +11 -3
  108. data/lib/masks/server/engine.rb +1 -0
  109. data/lib/masks/server/schema_reference.rb +17 -4
  110. data/lib/masks/server/tenancy/middleware.rb +9 -1
  111. data/lib/masks/server.rb +1 -0
  112. data/public/masks-assets/.vite/manifest.json +7 -7
  113. data/public/masks-assets/assets/app-D7W0gAyT.css +2 -0
  114. data/public/masks-assets/assets/{initial-D99AzFOd.js → initial-C1GpCiEh.js} +2 -2
  115. data/public/masks-assets/assets/{initial-D99AzFOd.js.map → initial-C1GpCiEh.js.map} +1 -1
  116. data/public/masks-assets/assets/login-C_6igRZ3.js +4 -0
  117. data/public/masks-assets/assets/login-C_6igRZ3.js.map +1 -0
  118. data/public/masks-assets/assets/manage-DR9qQiBU.js +569 -0
  119. data/public/masks-assets/assets/manage-DR9qQiBU.js.map +1 -0
  120. data/version.txt +1 -1
  121. metadata +14 -9
  122. data/app/views/masks/server/application/_person.html.erb +0 -27
  123. data/public/masks-assets/assets/app-DoQZrM_J.css +0 -2
  124. data/public/masks-assets/assets/login-CNs1UpxJ.js +0 -4
  125. data/public/masks-assets/assets/login-CNs1UpxJ.js.map +0 -1
  126. data/public/masks-assets/assets/manage-isLCrWiU.js +0 -567
  127. 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: 544dc01d0600507eb713667f0d8cf57275b4b7d957b603e3085f9e197e7e63c6
4
+ data.tar.gz: 70f664c959b40e98e9a273582a45c0dba352e5186430a72c26e3a8dd87db978b
5
5
  SHA512:
6
- metadata.gz: faeda2a8132f3e2e92ce8a1f41d235cae52f0640ad33ed3220d526619c5ecd113fece66af5923cb8e610564399ba3db0f22bdd16e9e0306d0155ba63f9e13f91
7
- data.tar.gz: bef27fdefa6bc7d1f88a386ac23261cad8b487137ea8130c0bfa41467f7bbec11b59058386ceb0fabe0f97cca1a914a1a3a9aaef6c74a48478b266e3a9c62858
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)
@@ -44,7 +45,7 @@ module Masks
44
45
  end
45
46
 
46
47
  def assertion_audiences
47
- [ issuer.url, "#{issuer.url}/token", "#{issuer.url}#{request.path}" ].uniq
48
+ [ issuer.url, "#{issuer.url}/token", "#{issuer.url}#{request.path_info}" ].uniq
48
49
  end
49
50
 
50
51
  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(
@@ -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
@@ -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] = {
@@ -263,6 +264,34 @@ module Masks
263
264
  )
264
265
  end
265
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
+
266
295
  def authorize_url_for(pending)
267
296
  "#{authorize_path}?#{URI.encode_www_form(pending.authorization.query_pairs)}"
268
297
  end
@@ -301,17 +330,18 @@ module Masks
301
330
  end
302
331
 
303
332
  def settle!(login)
304
- hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by)
333
+ hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
334
+ authenticated_at: login.authenticated_at)
305
335
  forget_login
306
336
  end
307
337
 
308
- def hold(actor, amr: [], policy: nil, first_factor: {})
338
+ def hold(actor, amr: [], policy: nil, first_factor: {}, authenticated_at: nil)
309
339
  return if actor.nil?
310
- return current_session.first_factored!(first_factor) if current_session&.actor_id == actor.id
340
+ return current_session.first_factored!(first_factor, at: authenticated_at) if current_session&.actor_id == actor.id
311
341
 
312
342
  sign_out if current_session
313
343
 
314
- sign_in(actor, amr: amr, policy: policy, first_factor: first_factor)
344
+ sign_in(actor, amr: amr, policy: policy, first_factor: first_factor, authenticated_at: authenticated_at)
315
345
  end
316
346
 
317
347
  def policy_denied(denial)
@@ -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)
@@ -63,9 +73,10 @@ module Masks
63
73
  req.bad_request!(:invalid_client, unknown_client(req.client_id)) if client.nil?
64
74
  req.invalid_request!("redirect_uri is required") if req.redirect_uri.blank?
65
75
 
66
- req.verify_redirect_uri!(client.redirect_uris)
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
  )
@@ -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.authenticated_at)
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
@@ -190,6 +191,7 @@ module Masks
190
191
  return refuse_device(pending) if login.refused? && pending&.device?
191
192
  return refuse_saml(pending) if login.refused? && pending&.saml?
192
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)
193
195
  return deny(pending, login.refusal.error, login.refusal.description) if login.refused? && pending
194
196
  return device_url_for(pending) if pending&.device?
195
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
@@ -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",
@@ -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
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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
@@ -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"/>'
@@ -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"],