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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/masks/server/client_authentication.rb +1 -1
- data/app/controllers/concerns/masks/server/resource_token.rb +12 -3
- data/app/controllers/masks/server/application_controller.rb +8 -6
- data/app/controllers/masks/server/authorize_controller.rb +1 -1
- data/app/controllers/masks/server/logins_controller.rb +2 -1
- data/app/controllers/masks/server/registrations_controller.rb +9 -1
- data/app/controllers/masks/server/scim/metadata_controller.rb +1 -1
- data/app/controllers/masks/server/tokens_controller.rb +1 -1
- data/app/graphql/masks/server/manage/mutations/add_app.rb +1 -1
- data/app/helpers/masks/server/account_helper.rb +1 -1
- data/app/models/masks/server/actor.rb +3 -3
- data/app/models/masks/server/client.rb +46 -6
- data/app/models/masks/server/handshake.rb +12 -6
- data/app/models/masks/server/login.rb +3 -6
- data/app/models/masks/server/pending_handshake.rb +2 -1
- data/app/models/masks/server/session.rb +7 -5
- data/app/policies/masks/server/exchange_policy.rb +1 -1
- data/app/policies/masks/server/grant_checks.rb +1 -1
- data/app/views/masks/server/account/_info.html.erb +1 -1
- data/app/views/masks/server/account/_organizations.html.erb +4 -4
- data/app/views/masks/server/application/_actor.html.erb +27 -0
- data/app/views/masks/server/handshakes/refused.html.erb +1 -1
- data/app/views/masks/server/handshakes/show.html.erb +1 -1
- data/app/views/masks/server/logins/_choose_organization.html.erb +1 -1
- data/app/views/masks/server/logins/_consent.html.erb +1 -1
- data/app/views/masks/server/sessions/confirm.html.erb +1 -1
- data/config/locales/de/account.yml +2 -2
- data/config/locales/de/application.yml +1 -1
- data/config/locales/de/events.yml +5 -5
- data/config/locales/de/manage/groups.yml +1 -1
- data/config/locales/de/manage/organization.yml +1 -1
- data/config/locales/en/account.yml +2 -2
- data/config/locales/en/application.yml +1 -1
- data/config/locales/en/events.yml +5 -5
- data/config/locales/en/manage/groups.yml +1 -1
- data/config/locales/en/manage/organization.yml +1 -1
- data/config/locales/es/account.yml +2 -2
- data/config/locales/es/application.yml +1 -1
- data/config/locales/es/events.yml +5 -5
- data/config/locales/es/manage/groups.yml +1 -1
- data/config/locales/es/manage/organization.yml +1 -1
- data/config/locales/fr/account.yml +2 -2
- data/config/locales/fr/application.yml +1 -1
- data/config/locales/fr/events.yml +5 -5
- data/config/locales/fr/manage/groups.yml +1 -1
- data/config/locales/fr/manage/organization.yml +1 -1
- data/db/migrate/20261013010000_add_handshake_return_to_to_clients.rb +5 -0
- data/lib/generators/masks/server/install/install_generator.rb +2 -2
- data/lib/masks/server/browser_access.rb +35 -0
- data/lib/masks/server/configuration.rb +3 -3
- data/lib/masks/server/engine.rb +1 -0
- data/lib/masks/server/schema_reference.rb +17 -4
- data/lib/masks/server.rb +1 -0
- data/public/masks-assets/.vite/manifest.json +7 -7
- data/public/masks-assets/assets/app-B27oPUll.css +2 -0
- data/public/masks-assets/assets/{initial-D99AzFOd.js → initial-C1GpCiEh.js} +2 -2
- data/public/masks-assets/assets/{initial-D99AzFOd.js.map → initial-C1GpCiEh.js.map} +1 -1
- data/public/masks-assets/assets/login-C_6igRZ3.js +4 -0
- data/public/masks-assets/assets/login-C_6igRZ3.js.map +1 -0
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js +567 -0
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +1 -0
- data/version.txt +1 -1
- metadata +11 -9
- data/app/views/masks/server/application/_person.html.erb +0 -27
- data/public/masks-assets/assets/app-DoQZrM_J.css +0 -2
- data/public/masks-assets/assets/login-CNs1UpxJ.js +0 -4
- data/public/masks-assets/assets/login-CNs1UpxJ.js.map +0 -1
- data/public/masks-assets/assets/manage-isLCrWiU.js +0 -567
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36e33f34e755bbbb4470a5c55b920624f2042678b87c810ce614e86f9638a4df
|
|
4
|
+
data.tar.gz: 2bf178807ba578015922b44145ad0eb28454698adb877ac51b5059be57a35ca3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d32dd6592c56c0132971c1f322ca90b2a31df6832c0b9fe33d5c41c7d5d7aab443b4c6b2fec0a9d3ac0635b22b6d86bbda1c451bd791fd3794e4776c1edd1c8
|
|
7
|
+
data.tar.gz: 59f5658c277590947cf74f3b8e57fe2d9d7b01f1a160bdc3bdbc3593f4aba9b9395b08255b866e54029f48aff6d9b7b1384daf9ab1b21b95f7468678a800fbcb
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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}/
|
|
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 },
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
200
|
-
"role" => (I18n.t("application.
|
|
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.
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
433
|
-
|
|
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
|
-
|
|
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!(
|
|
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 =
|
|
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
|
|
@@ -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(:
|
|
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.
|
|
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
|
-
<%
|
|
85
|
-
<% yourself =
|
|
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",
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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">
|
|
@@ -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
|
-
|
|
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 %{
|
|
185
|
+
role_of: "Rolle von %{name}"
|
|
186
186
|
remove: "Entfernen"
|
|
187
187
|
withdraw: "Zurückziehen"
|
|
188
188
|
resend: "Erneut senden"
|