masks 0.12.1 → 0.12.2
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/CHANGELOG.md +8 -0
- data/app/controllers/masks/rails/sessions_controller.rb +13 -1
- data/lib/masks/client/issuer.rb +37 -9
- data/lib/masks/client/session.rb +8 -3
- data/lib/masks/client/verifier.rb +13 -4
- data/lib/masks/rails/authentication.rb +5 -2
- data/lib/masks/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 638e4da3d119c8e1841e1d8f4d87742133ac14ef28c8b1f7dbbdd55725913974
|
|
4
|
+
data.tar.gz: 5c704c90104b9fa581948029a2897e40e1e69f66573739f33aa6c3b7ceae14d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97f6edc170dc1544af96fa847f7513a547a39422fffa2435c44f8073bcfaaaf205bf60ed4fc1f1bf82b50c968e9b21e7ff88558fc2042039df5be4ab332b8174
|
|
7
|
+
data.tar.gz: f0c0b5d58b797556ef18dffdf3a81b09e9f94407c68da642950c4578639376a109064b3efbb24d5c802edef0c138a2f079002229f35d9ae520bc2c98ff0c5439
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.2](https://github.com/masksrb/masks/compare/gem/v0.12.1...gem/v0.12.2) (2026-10-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixes
|
|
7
|
+
|
|
8
|
+
* **client:** a sign-in older than the max_age the app asked for is refused ([7116e24](https://github.com/masksrb/masks/commit/7116e24e73dedcca6ce54aa4347100b7d4714e3a)), closes [#161](https://github.com/masksrb/masks/issues/161)
|
|
9
|
+
* **client:** the last good issuer and keys are served while masks is down ([1423d1e](https://github.com/masksrb/masks/commit/1423d1ea6b7968aa66ad2a2ca63180effa4c1002)), closes [#162](https://github.com/masksrb/masks/issues/162)
|
|
10
|
+
|
|
3
11
|
## [0.12.1](https://github.com/masksrb/masks/compare/gem/v0.12.0...gem/v0.12.1) (2026-10-19)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -35,12 +35,13 @@ module Masks
|
|
|
35
35
|
state: pending.id,
|
|
36
36
|
resource: masks_config.resource_for(request),
|
|
37
37
|
scope: masks_config.scope_for(params[:privilege]),
|
|
38
|
+
max_age: (params[:max_age].to_i if params[:max_age].to_i.positive?),
|
|
38
39
|
organization: Masks::Client::Session.organization_key(params[:organization]) ||
|
|
39
40
|
masks_config.organization_for(request)
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
masks_requests.amend(
|
|
43
|
-
pending.id, nonce: started[:nonce], verifier: started[:verifier]
|
|
44
|
+
pending.id, nonce: started[:nonce], verifier: started[:verifier], max_age: started[:max_age]
|
|
44
45
|
)
|
|
45
46
|
|
|
46
47
|
redirect_to started[:url], allow_other_host: true
|
|
@@ -69,6 +70,10 @@ module Masks
|
|
|
69
70
|
return refuse("invalid_nonce", "the id token was issued for another request", pending)
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
unless recent?(identity, pending[:max_age])
|
|
74
|
+
return refuse("invalid_token", "the person signed in longer ago than max_age allows", pending)
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
renew_session
|
|
73
78
|
masks_store(tokens, identity: identity)
|
|
74
79
|
|
|
@@ -156,6 +161,13 @@ module Masks
|
|
|
156
161
|
masks_local_path(params[:return_to])
|
|
157
162
|
end
|
|
158
163
|
|
|
164
|
+
def recent?(identity, max_age)
|
|
165
|
+
Masks::Client::Verifier.recent!(identity.is_a?(Hash) ? identity : {}, max_age)
|
|
166
|
+
true
|
|
167
|
+
rescue Masks::Client::InvalidToken
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
159
171
|
def nonce_matches?(identity, sent)
|
|
160
172
|
return false if identity == :unverified
|
|
161
173
|
return true if sent.blank?
|
data/lib/masks/client/issuer.rb
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
module Masks
|
|
2
2
|
module Client
|
|
3
|
+
# One issuer's discovery document and keys, fetched once and kept for an
|
|
4
|
+
# hour. While masks cannot be reached, the last good copies are served, a
|
|
5
|
+
# token signed with a key never seen raises Unreachable, and refetches back
|
|
6
|
+
# off from 5 seconds to once a minute.
|
|
3
7
|
class Issuer
|
|
4
8
|
DISCOVERY_PATH = "/.well-known/openid-configuration".freeze
|
|
5
|
-
|
|
9
|
+
LIFETIME = 3600
|
|
10
|
+
FIRST_BACKOFF = 5
|
|
11
|
+
LONGEST_BACKOFF = 60
|
|
12
|
+
TTL = LIFETIME
|
|
6
13
|
|
|
7
14
|
def self.normalize(url)
|
|
8
15
|
url.to_s.chomp("/")
|
|
@@ -44,6 +51,10 @@ module Masks
|
|
|
44
51
|
fetch(:jwks) { HTTP.get(endpoint("jwks_uri")) }
|
|
45
52
|
end
|
|
46
53
|
|
|
54
|
+
def keys!
|
|
55
|
+
fetch(:jwks, now: true) { HTTP.get(endpoint("jwks_uri")) }
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
def tenant
|
|
48
59
|
discovery["tenant"]
|
|
49
60
|
end
|
|
@@ -73,24 +84,41 @@ module Masks
|
|
|
73
84
|
end
|
|
74
85
|
|
|
75
86
|
def refresh!
|
|
76
|
-
@lock.synchronize { @cache =
|
|
87
|
+
@lock.synchronize { @cache.each_value { |held| held[:at] = 0 } }
|
|
77
88
|
self
|
|
78
89
|
end
|
|
79
90
|
|
|
80
91
|
private
|
|
81
92
|
|
|
82
|
-
def fetch(key)
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
def fetch(key, now: false, &fetching)
|
|
94
|
+
held = @lock.synchronize { @cache[key] ||= { backoff: FIRST_BACKOFF, retry_at: 0 } }
|
|
95
|
+
clock = Time.now.to_i
|
|
96
|
+
good = held.key?(:value)
|
|
97
|
+
|
|
98
|
+
return held[:value] if good && !now && (held[:at] + @ttl > clock || clock < held[:retry_at])
|
|
99
|
+
raise held[:failure] if clock < held[:retry_at]
|
|
100
|
+
|
|
101
|
+
refetch(held, clock, keep: good && !now, &fetching)
|
|
102
|
+
end
|
|
85
103
|
|
|
104
|
+
def refetch(held, clock, keep:)
|
|
86
105
|
value = yield
|
|
87
106
|
|
|
88
|
-
@lock.synchronize
|
|
107
|
+
@lock.synchronize do
|
|
108
|
+
held.merge!(value: value, at: clock, failure: nil, retry_at: 0, backoff: FIRST_BACKOFF)
|
|
109
|
+
end
|
|
110
|
+
|
|
89
111
|
value
|
|
90
|
-
|
|
112
|
+
rescue Masks::Client::Error => e
|
|
113
|
+
@lock.synchronize do
|
|
114
|
+
held[:failure] = e
|
|
115
|
+
held[:retry_at] = clock + held[:backoff]
|
|
116
|
+
held[:backoff] = [ held[:backoff] * 2, LONGEST_BACKOFF ].min
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
raise e unless keep
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
entry && entry[:at] + @ttl > Time.now.to_i
|
|
121
|
+
held[:value]
|
|
94
122
|
end
|
|
95
123
|
end
|
|
96
124
|
end
|
data/lib/masks/client/session.rb
CHANGED
|
@@ -60,7 +60,8 @@ module Masks
|
|
|
60
60
|
url: "#{issuer.endpoint('authorization_endpoint')}?#{URI.encode_www_form(pairs)}",
|
|
61
61
|
state: state,
|
|
62
62
|
nonce: nonce,
|
|
63
|
-
verifier: pkce.verifier
|
|
63
|
+
verifier: pkce.verifier,
|
|
64
|
+
max_age: max_age&.to_i
|
|
64
65
|
}
|
|
65
66
|
end
|
|
66
67
|
|
|
@@ -215,10 +216,14 @@ module Masks
|
|
|
215
216
|
HTTP.parse(Transport.new(url, tokens: tokens, key: dpop).request("GET", url, limit: HTTP::READ_LIMIT))
|
|
216
217
|
end
|
|
217
218
|
|
|
218
|
-
|
|
219
|
+
# The person the +tokens+ belong to: the ID token's claims merged over
|
|
220
|
+
# userinfo. With +max_age+, the seconds the app asked for when it started
|
|
221
|
+
# the sign-in, an ID token whose +auth_time+ is missing or older than
|
|
222
|
+
# that, plus 30 seconds of leeway, raises InvalidToken.
|
|
223
|
+
def identity(tokens, max_age: nil)
|
|
219
224
|
return nil if tokens.id_token.nil?
|
|
220
225
|
|
|
221
|
-
claims = Verifier.new(issuer, audience: client_id).verify(tokens.id_token)
|
|
226
|
+
claims = Verifier.recent!(Verifier.new(issuer, audience: client_id).verify(tokens.id_token), max_age)
|
|
222
227
|
|
|
223
228
|
profile(tokens).merge(claims)
|
|
224
229
|
end
|
|
@@ -5,6 +5,18 @@ module Masks
|
|
|
5
5
|
ACCESS_TOKEN = "at+jwt".freeze
|
|
6
6
|
LOGOUT_TOKEN = "logout+jwt".freeze
|
|
7
7
|
ID_TOKEN = [ "jwt", nil ].freeze
|
|
8
|
+
AUTH_TIME_LEEWAY = 30
|
|
9
|
+
|
|
10
|
+
def self.recent!(claims, max_age)
|
|
11
|
+
return claims if max_age.nil?
|
|
12
|
+
|
|
13
|
+
signed_in_at = claims["auth_time"]
|
|
14
|
+
allowed = signed_in_at.is_a?(Numeric) && signed_in_at + max_age.to_i + AUTH_TIME_LEEWAY >= Time.now.to_i
|
|
15
|
+
|
|
16
|
+
raise InvalidToken, "the person signed in longer ago than max_age allows" unless allowed
|
|
17
|
+
|
|
18
|
+
claims
|
|
19
|
+
end
|
|
8
20
|
|
|
9
21
|
attr_reader :issuer, :audience
|
|
10
22
|
|
|
@@ -51,10 +63,7 @@ module Masks
|
|
|
51
63
|
end
|
|
52
64
|
|
|
53
65
|
def keys
|
|
54
|
-
->(options)
|
|
55
|
-
issuer.refresh! if options[:invalidate]
|
|
56
|
-
issuer.jwks
|
|
57
|
-
end
|
|
66
|
+
->(options) { options[:invalidate] ? issuer.keys! : issuer.jwks }
|
|
58
67
|
end
|
|
59
68
|
end
|
|
60
69
|
end
|
|
@@ -194,12 +194,15 @@ module Masks
|
|
|
194
194
|
held.merge(profile.slice(*IDENTITY))
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
# Where to send someone to sign in. +max_age+ asks masks for a sign-in no
|
|
198
|
+
# older than that many seconds; an older one is refused at the callback.
|
|
199
|
+
def masks_login_url(return_to: nil, organization: nil, privilege: nil, max_age: nil)
|
|
198
200
|
path = Masks::Rails::Engine.routes.url_helpers.start_path
|
|
199
201
|
query = {
|
|
200
202
|
"return_to" => masks_local_path(return_to),
|
|
201
203
|
"organization" => Masks::Client::Session.organization_key(organization),
|
|
202
|
-
"privilege" => Array(privilege).join(" ").presence
|
|
204
|
+
"privilege" => Array(privilege).join(" ").presence,
|
|
205
|
+
"max_age" => (max_age.to_i if max_age.to_i.positive?)
|
|
203
206
|
}.compact
|
|
204
207
|
|
|
205
208
|
query.empty? ? path : "#{path}?#{URI.encode_www_form(query)}"
|
data/lib/masks/version.rb
CHANGED