volcano-sdk 0.28.0 → 0.30.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/lib/volcano/auth.rb +7 -0
- data/lib/volcano/auth_email_change.rb +1 -1
- data/lib/volcano/auth_get_user.rb +36 -77
- data/lib/volcano/auth_notification_dispatch.rb +16 -0
- data/lib/volcano/auth_oauth.rb +4 -0
- data/lib/volcano/auth_oauth_api.rb +8 -3
- data/lib/volcano/auth_oauth_sign_in.rb +1 -1
- data/lib/volcano/auth_oauth_token.rb +2 -0
- data/lib/volcano/auth_profile_fields.rb +98 -0
- data/lib/volcano/auth_refresh.rb +29 -32
- data/lib/volcano/auth_session_binding.rb +25 -0
- data/lib/volcano/auth_sessions.rb +9 -45
- data/lib/volcano/auth_sign_out.rb +22 -14
- data/lib/volcano/auth_state.rb +3 -2
- data/lib/volcano/auth_state_notifications.rb +7 -4
- data/lib/volcano/auth_update_user.rb +1 -1
- data/lib/volcano/generated_transport.rb +6 -3
- data/lib/volcano/lock_response.rb +48 -0
- data/lib/volcano/locks.rb +57 -33
- data/lib/volcano/session.rb +0 -73
- data/lib/volcano/session_credentials.rb +82 -0
- data/lib/volcano/session_operations.rb +13 -6
- data/lib/volcano/session_page_mapping.rb +89 -0
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +14 -7
- data/sig/auth_oauth.rbs +16 -0
- data/sig/auth_profile.rbs +10 -0
- data/sig/auth_sessions.rbs +7 -0
- data/sig/auth_subscription.rbs +6 -0
- data/sig/session.rbs +1 -1
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 296cdb524e3552873235adccfe2d5c98b39a3da451c06777020b59d45c6a4be0
|
|
4
|
+
data.tar.gz: 9ca783889458304d91b40601f673a4f5015f117ee4f30f90c23891e351cafe3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81067b14e84482e5dfebf935d27051c109ff14b87d144814a36db6ef5a7de6003e0cde0fe4ed4c54c1e06dcd9db79d8f5088c1aa21fbd4bbc3419da9a585f44e
|
|
7
|
+
data.tar.gz: 4346575e985ab233b5afd723f36e1044b4e9e001b1eefa461a2514977f10a240a88a73978f30d88b2b74a3ec2a6bd4565d1e1ab92968e1988069492cbae9c739
|
data/lib/volcano/auth.rb
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Authenticates users and updates the client session.
|
|
5
5
|
class Auth
|
|
6
|
+
include AuthRefresh
|
|
7
|
+
include AuthSignOut
|
|
8
|
+
include AuthProfile
|
|
9
|
+
include AuthUpdateUser
|
|
10
|
+
include AuthEmailChange
|
|
11
|
+
include AuthSessions
|
|
12
|
+
|
|
6
13
|
INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
|
|
7
14
|
private_constant :INCOMPLETE_SESSION
|
|
8
15
|
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'time'
|
|
4
|
-
|
|
5
3
|
module Volcano
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
RFC3339_OFFSET = /(?:[Zz]|[+-]\d{2}:\d{2})\z/
|
|
10
|
-
USER_OPTIONAL_VALUES = %w[project_id email_confirmed user_metadata app_metadata avatar_url].freeze
|
|
11
|
-
USER_OPTIONAL_STRINGS = %w[project_id avatar_url].freeze
|
|
12
|
-
USER_STATUSES = %w[active banned deleted].freeze
|
|
13
|
-
USER_TIMESTAMPS = %w[banned_until last_sign_in_at created_at updated_at].freeze
|
|
14
|
-
private_constant :INVALID_USER, :RFC3339_OFFSET, :USER_OPTIONAL_STRINGS,
|
|
15
|
-
:USER_OPTIONAL_VALUES, :USER_STATUSES, :USER_TIMESTAMPS
|
|
4
|
+
# Current-user requests and session cache updates.
|
|
5
|
+
module AuthProfile
|
|
6
|
+
include AuthProfileFields
|
|
16
7
|
|
|
17
8
|
def user
|
|
18
|
-
profile_request {
|
|
9
|
+
profile_request { ->(token) { get_user_response(token) } }
|
|
19
10
|
end
|
|
20
11
|
|
|
21
12
|
alias get_user user
|
|
@@ -32,7 +23,7 @@ module Volcano
|
|
|
32
23
|
end
|
|
33
24
|
|
|
34
25
|
def cache_current_user(payload, generation)
|
|
35
|
-
profile =
|
|
26
|
+
profile = profile_user_payload(payload)
|
|
36
27
|
user = build_user(profile)
|
|
37
28
|
raise Error::SessionChangedError unless @client.update_session_user_if_current?(profile, generation)
|
|
38
29
|
|
|
@@ -46,79 +37,47 @@ module Volcano
|
|
|
46
37
|
end
|
|
47
38
|
|
|
48
39
|
def build_user(payload)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
User.new(**user_attributes(payload))
|
|
40
|
+
new_profile_user(user_identity(payload), user_optional_attributes(payload), user_timestamps(payload))
|
|
52
41
|
rescue ArgumentError => e
|
|
53
42
|
raise Error::AuthenticationError, INVALID_USER, cause: e
|
|
54
43
|
end
|
|
55
44
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
id:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def user_payload(payload)
|
|
67
|
-
raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
|
|
68
|
-
|
|
69
|
-
payload['user']
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def valid_user?(payload)
|
|
73
|
-
return false unless payload.is_a?(Hash)
|
|
74
|
-
|
|
75
|
-
required_user_fields?(payload) && valid_optional_user_fields?(payload)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def valid_optional_user_fields?(payload)
|
|
79
|
-
valid_email_confirmation?(payload) && valid_metadata?(payload) &&
|
|
80
|
-
valid_optional_strings?(payload) && valid_timestamps?(payload)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def required_user_fields?(payload)
|
|
84
|
-
complete_string?(payload['id']) && payload['email'].is_a?(String) &&
|
|
85
|
-
USER_STATUSES.include?(payload['status'])
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def valid_email_confirmation?(payload)
|
|
89
|
-
[true, false, nil].include?(payload['email_confirmed'])
|
|
45
|
+
def new_profile_user(identity, optional, timestamps)
|
|
46
|
+
User.new(
|
|
47
|
+
id: identity[:id], email: identity[:email], status: identity[:status],
|
|
48
|
+
project_id: optional[:project_id], email_confirmed: optional[:email_confirmed],
|
|
49
|
+
user_metadata: optional[:user_metadata], app_metadata: optional[:app_metadata],
|
|
50
|
+
avatar_url: optional[:avatar_url], banned_until: timestamps[:banned_until],
|
|
51
|
+
last_sign_in_at: timestamps[:last_sign_in_at], created_at: timestamps[:created_at],
|
|
52
|
+
updated_at: timestamps[:updated_at]
|
|
53
|
+
)
|
|
90
54
|
end
|
|
91
55
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def valid_optional_strings?(payload)
|
|
99
|
-
USER_OPTIONAL_STRINGS.all? do |name|
|
|
100
|
-
payload[name].nil? || payload[name].is_a?(String)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def valid_timestamps?(payload)
|
|
105
|
-
USER_TIMESTAMPS.all? { |name| valid_timestamp?(payload[name]) }
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def valid_timestamp?(value)
|
|
109
|
-
return true if value.nil?
|
|
110
|
-
|
|
111
|
-
value.is_a?(String) && RFC3339_OFFSET.match?(value) && Time.iso8601(value)
|
|
112
|
-
rescue ArgumentError
|
|
113
|
-
false
|
|
56
|
+
def user_identity(payload)
|
|
57
|
+
{
|
|
58
|
+
id: profile_required_id(payload['id']),
|
|
59
|
+
email: profile_required_string(payload['email']),
|
|
60
|
+
status: profile_status(payload['status'])
|
|
61
|
+
}
|
|
114
62
|
end
|
|
115
63
|
|
|
116
|
-
def
|
|
117
|
-
|
|
64
|
+
def user_optional_attributes(payload)
|
|
65
|
+
{
|
|
66
|
+
project_id: profile_optional_string(payload['project_id']),
|
|
67
|
+
email_confirmed: profile_optional_boolean(payload['email_confirmed']),
|
|
68
|
+
user_metadata: profile_optional_object(payload['user_metadata']),
|
|
69
|
+
app_metadata: profile_optional_object(payload['app_metadata']),
|
|
70
|
+
avatar_url: profile_optional_string(payload['avatar_url'])
|
|
71
|
+
}
|
|
118
72
|
end
|
|
119
73
|
|
|
120
|
-
def
|
|
121
|
-
|
|
74
|
+
def user_timestamps(payload)
|
|
75
|
+
{
|
|
76
|
+
banned_until: profile_parse_time(payload['banned_until']),
|
|
77
|
+
last_sign_in_at: profile_parse_time(payload['last_sign_in_at']),
|
|
78
|
+
created_at: profile_parse_time(payload['created_at']),
|
|
79
|
+
updated_at: profile_parse_time(payload['updated_at'])
|
|
80
|
+
}
|
|
122
81
|
end
|
|
123
82
|
end
|
|
124
83
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Runs queued auth-state callbacks outside session coordination locks.
|
|
5
|
+
module AuthNotificationDispatch
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_auth_notifications(notifications)
|
|
9
|
+
notifications.each do |notification|
|
|
10
|
+
notification.call
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
private_constant :AuthNotificationDispatch
|
|
16
|
+
end
|
data/lib/volcano/auth_oauth.rb
CHANGED
|
@@ -88,5 +88,9 @@ module Volcano
|
|
|
88
88
|
def valid_linked_oauth_provider?(provider, linked_at, updated_at)
|
|
89
89
|
complete_string?(provider) && linked_at.is_a?(Time) && updated_at.is_a?(Time)
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
def complete_string?(value)
|
|
93
|
+
value.is_a?(String) && !value.strip.empty?
|
|
94
|
+
end
|
|
91
95
|
end
|
|
92
96
|
end
|
|
@@ -6,9 +6,12 @@ module Volcano
|
|
|
6
6
|
def call_oauth_api(provider, endpoint:, method: 'GET', body: nil)
|
|
7
7
|
session_payload(200, decode: :oauth_api_data) do
|
|
8
8
|
provider_name = oauth_provider_name(provider)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
owned_endpoint = endpoint.dup.freeze
|
|
10
|
+
owned_method = method.dup.freeze
|
|
11
|
+
owned_body = body.nil? ? nil : JSON.parse(JSON.generate(body), freeze: true)
|
|
12
|
+
lambda do |token|
|
|
13
|
+
oauth_api_response(token, provider_name, endpoint: owned_endpoint, method: owned_method, body: owned_body)
|
|
14
|
+
end
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
|
|
@@ -24,6 +27,8 @@ module Volcano
|
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def oauth_api_data(response_body)
|
|
30
|
+
raise TypeError, 'Expected OAuth provider API response data' unless response_body.is_a?(Hash)
|
|
31
|
+
|
|
27
32
|
data = response_body.fetch('data')
|
|
28
33
|
freeze_oauth_api_data(data)
|
|
29
34
|
rescue KeyError, NoMethodError
|
|
@@ -20,7 +20,7 @@ module Volcano
|
|
|
20
20
|
def exchange_oauth_code(code:, redirect_to:, state:, expected_state:)
|
|
21
21
|
validate_oauth_callback_state(state, expected_state)
|
|
22
22
|
generation, = @client.capture_session
|
|
23
|
-
session = build_session(Transport.body(oauth_exchange_response(code, redirect_to), 200))
|
|
23
|
+
session = owned_complete_session(build_session(Transport.body(oauth_exchange_response(code, redirect_to), 200)))
|
|
24
24
|
raise Error::SessionChangedError unless @client.store_session_if_current?(session, generation)
|
|
25
25
|
|
|
26
26
|
session
|
|
@@ -32,6 +32,8 @@ module Volcano
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def oauth_provider_token_status(body)
|
|
35
|
+
raise TypeError, 'Expected complete OAuth provider token status' unless body.is_a?(Hash)
|
|
36
|
+
|
|
35
37
|
message, provider, expires_in = body.fetch_values('message', 'provider', 'expires_in')
|
|
36
38
|
OAuthProviderTokenStatus.new(message:, provider:, expires_in:)
|
|
37
39
|
rescue KeyError, NoMethodError
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Volcano
|
|
6
|
+
# Validates and maps the current-user JSON response.
|
|
7
|
+
module AuthProfileFields
|
|
8
|
+
INVALID_USER = 'Expected a complete user profile'
|
|
9
|
+
RFC3339_OFFSET = /(?:[Zz]|[+-]\d{2}:\d{2})\z/
|
|
10
|
+
USER_STATUSES = %w[active banned deleted].freeze
|
|
11
|
+
private_constant :INVALID_USER, :RFC3339_OFFSET, :USER_STATUSES
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def profile_user_payload(payload)
|
|
16
|
+
raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
profile_json_object(payload['user'])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def profile_json_object(payload)
|
|
22
|
+
raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
payload.to_h do |key, value|
|
|
25
|
+
raise Error::AuthenticationError, INVALID_USER unless key.is_a?(String)
|
|
26
|
+
|
|
27
|
+
[key, profile_json_value(value)]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def profile_json_value(value)
|
|
32
|
+
case value
|
|
33
|
+
when Array then value.map { |item| profile_json_value(item) }
|
|
34
|
+
when Hash then profile_json_object(value)
|
|
35
|
+
else profile_json_scalar(value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def profile_json_scalar(value)
|
|
40
|
+
case value
|
|
41
|
+
when nil, true, false, Integer, String then value
|
|
42
|
+
when Float then profile_finite_float(value)
|
|
43
|
+
else raise Error::AuthenticationError, INVALID_USER
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def profile_finite_float(value)
|
|
48
|
+
raise Error::AuthenticationError, INVALID_USER unless value.finite?
|
|
49
|
+
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def profile_required_string(value)
|
|
54
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String)
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def profile_required_id(value)
|
|
60
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && !value.strip.empty?
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def profile_status(value)
|
|
66
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && USER_STATUSES.include?(value)
|
|
67
|
+
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def profile_optional_string(value)
|
|
72
|
+
return if value.nil?
|
|
73
|
+
|
|
74
|
+
profile_required_string(value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def profile_optional_object(value)
|
|
78
|
+
profile_json_object(value) unless value.nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def profile_optional_boolean(value)
|
|
82
|
+
return if value.nil?
|
|
83
|
+
return true if value == true
|
|
84
|
+
return false if value == false
|
|
85
|
+
|
|
86
|
+
raise Error::AuthenticationError, INVALID_USER
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def profile_parse_time(value)
|
|
90
|
+
return if value.nil?
|
|
91
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && RFC3339_OFFSET.match?(value)
|
|
92
|
+
|
|
93
|
+
Time.iso8601(value).freeze
|
|
94
|
+
rescue ArgumentError
|
|
95
|
+
raise Error::AuthenticationError, INVALID_USER
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/volcano/auth_refresh.rb
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Refreshes only the session captured by the initiating operation.
|
|
5
|
-
|
|
5
|
+
module AuthRefresh
|
|
6
|
+
include AuthNotificationDispatch
|
|
7
|
+
include AuthSessionBinding
|
|
8
|
+
|
|
6
9
|
def refresh_session
|
|
7
10
|
binding = @client.capture_session_binding
|
|
8
|
-
|
|
11
|
+
session = binding.last
|
|
12
|
+
raise Error::AuthenticationError, 'No active session' unless session
|
|
9
13
|
|
|
10
|
-
refresh_captured_session(binding)
|
|
14
|
+
refresh_captured_session([binding.first, binding[1], session])
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def session_request(binding: @client.capture_session_binding)
|
|
@@ -26,7 +30,7 @@ module Volcano
|
|
|
26
30
|
response
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
def session_read(...) = session_request(...)
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
@@ -51,6 +55,7 @@ module Volcano
|
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
def refresh_with_notifications(binding)
|
|
58
|
+
# @type var notifications: Array[Method]
|
|
54
59
|
notifications = []
|
|
55
60
|
active_generation, = owned_session_binding(binding)
|
|
56
61
|
binding[1].refresh { perform_refresh(binding, notifications) } if active_generation == binding.first
|
|
@@ -60,31 +65,17 @@ module Volcano
|
|
|
60
65
|
raise
|
|
61
66
|
ensure
|
|
62
67
|
# Only the dispatcher owner drains, after refresh coordination is released.
|
|
63
|
-
notifications
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def owned_session_binding(binding)
|
|
67
|
-
active = @client.capture_session_binding
|
|
68
|
-
raise Error::AuthenticationError, 'No active session' if rejected_refresh?(binding, active)
|
|
69
|
-
|
|
70
|
-
_, active_lineage, current = active
|
|
71
|
-
raise Error::SessionChangedError unless current && active_lineage == binding[1]
|
|
72
|
-
|
|
73
|
-
active
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def rejected_refresh?(binding, active)
|
|
77
|
-
generation, lineage, = binding
|
|
78
|
-
@client.refresh_rejected?(generation, lineage) &&
|
|
79
|
-
active.first == generation + 1 && active.last.nil?
|
|
68
|
+
run_auth_notifications(notifications)
|
|
80
69
|
end
|
|
81
70
|
|
|
82
71
|
def perform_refresh(binding, notifications)
|
|
83
72
|
active = owned_session_binding(binding)
|
|
84
73
|
return active.last if active.first != binding.first
|
|
85
|
-
raise Error::AuthenticationError, 'No refresh token' unless binding.last.refresh_token
|
|
86
74
|
|
|
87
|
-
|
|
75
|
+
refresh_token = binding.last.refresh_token
|
|
76
|
+
raise Error::AuthenticationError, 'No refresh token' unless refresh_token
|
|
77
|
+
|
|
78
|
+
session = refreshed_session(binding, refresh_token, notifications)
|
|
88
79
|
unless binding[1].closing?
|
|
89
80
|
@client.store_session_if_current?(
|
|
90
81
|
session, binding.first, event: :token_refreshed, notifications: notifications
|
|
@@ -93,17 +84,23 @@ module Volcano
|
|
|
93
84
|
session
|
|
94
85
|
end
|
|
95
86
|
|
|
96
|
-
def refreshed_session(binding, notifications)
|
|
87
|
+
def refreshed_session(binding, refresh_token, notifications)
|
|
97
88
|
_, owner, current = binding
|
|
98
89
|
verified = owner.verified_pair?(current)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
begin
|
|
91
|
+
SessionCredentials.validate_refresh_source(current, verified: verified)
|
|
92
|
+
owner.verify_pair(nil)
|
|
93
|
+
parse_and_verify_refresh(owner, current, binding, refresh_token, notifications)
|
|
94
|
+
rescue Error::RateLimitedError
|
|
95
|
+
owner.verify_pair(current) if verified
|
|
96
|
+
raise
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_and_verify_refresh(owner, current, binding, refresh_token, notifications)
|
|
101
|
+
session = parse_refresh_session(refresh_payload(binding, refresh_token, notifications))
|
|
102
102
|
verify_refreshed_session(owner, current, session)
|
|
103
103
|
session
|
|
104
|
-
rescue Error::RateLimitedError
|
|
105
|
-
owner.verify_pair(current) if verified
|
|
106
|
-
raise
|
|
107
104
|
end
|
|
108
105
|
|
|
109
106
|
def verify_refreshed_session(owner, current, session)
|
|
@@ -111,8 +108,8 @@ module Volcano
|
|
|
111
108
|
owner.verify_pair(session)
|
|
112
109
|
end
|
|
113
110
|
|
|
114
|
-
def refresh_payload(binding, notifications)
|
|
115
|
-
Transport.body(refresh_response(
|
|
111
|
+
def refresh_payload(binding, refresh_token, notifications)
|
|
112
|
+
Transport.body(refresh_response(refresh_token), 200)
|
|
116
113
|
rescue Error::AuthenticationError
|
|
117
114
|
generation, owner, = binding
|
|
118
115
|
@client.reject_refresh_if_current?(generation, notifications: notifications) unless owner.closing?
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Rejects stale or replaced auth-session lineages.
|
|
5
|
+
module AuthSessionBinding
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def owned_session_binding(binding)
|
|
9
|
+
active = @client.capture_session_binding
|
|
10
|
+
raise Error::AuthenticationError, 'No active session' if rejected_refresh?(binding, active)
|
|
11
|
+
|
|
12
|
+
_, active_lineage, current = active
|
|
13
|
+
raise Error::SessionChangedError unless current && active_lineage == binding[1]
|
|
14
|
+
|
|
15
|
+
[active.first, active_lineage, current]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rejected_refresh?(binding, active)
|
|
19
|
+
generation, lineage, = binding
|
|
20
|
+
@client.refresh_rejected?(generation, lineage) &&
|
|
21
|
+
active.first == generation + 1 && active.last.nil?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
private_constant :AuthSessionBinding
|
|
25
|
+
end
|
|
@@ -1,49 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative 'session_page_mapping'
|
|
4
4
|
|
|
5
5
|
module Volcano
|
|
6
|
-
# Maps the internal wire response to immutable public session values.
|
|
7
|
-
module SessionPageMapping
|
|
8
|
-
SESSION_FIELDS = %i[
|
|
9
|
-
id user_id provider expires_at is_active is_current user_agent ip_address last_ip_address
|
|
10
|
-
last_activity_at session_started_at created_at updated_at
|
|
11
|
-
].freeze
|
|
12
|
-
REQUIRED_SESSION_FIELDS = SESSION_FIELDS.first(6).freeze
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def session_page(body)
|
|
17
|
-
raise TypeError, 'Expected a complete session page' unless body.is_a?(Hash)
|
|
18
|
-
|
|
19
|
-
sessions = body.fetch('sessions')
|
|
20
|
-
raise TypeError, 'Expected a complete session page' unless sessions.is_a?(Array)
|
|
21
|
-
|
|
22
|
-
SessionPage.new(
|
|
23
|
-
sessions: sessions.map { |attributes| auth_session(attributes) },
|
|
24
|
-
total: body.fetch('total'), page: body.fetch('page'), limit: body.fetch('limit'),
|
|
25
|
-
total_pages: body.fetch('total_pages')
|
|
26
|
-
)
|
|
27
|
-
rescue KeyError
|
|
28
|
-
raise TypeError, 'Expected a complete session page'
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def auth_session(attributes)
|
|
32
|
-
raise TypeError, 'Expected a complete authentication session' unless attributes.is_a?(Hash)
|
|
33
|
-
|
|
34
|
-
required = REQUIRED_SESSION_FIELDS.to_h { |name| [name, attributes.fetch(name.to_s)] }
|
|
35
|
-
optional = (SESSION_FIELDS - REQUIRED_SESSION_FIELDS).to_h do |name|
|
|
36
|
-
[name, attributes[name.to_s]]
|
|
37
|
-
end
|
|
38
|
-
AuthSession.new(**required, **optional)
|
|
39
|
-
rescue KeyError
|
|
40
|
-
raise TypeError, 'Expected a complete authentication session'
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
private_constant :SessionPageMapping
|
|
44
|
-
|
|
45
6
|
# Multi-device session behavior for the authentication facade.
|
|
46
|
-
|
|
7
|
+
module AuthSessions
|
|
47
8
|
include SessionPageMapping
|
|
48
9
|
|
|
49
10
|
def list_sessions(page: 1, limit: 20)
|
|
@@ -57,10 +18,11 @@ module Volcano
|
|
|
57
18
|
|
|
58
19
|
def delete_session(session_id)
|
|
59
20
|
binding = @client.capture_session_binding
|
|
60
|
-
|
|
21
|
+
session = binding.last
|
|
22
|
+
raise Error::AuthenticationError, 'No active session' unless session
|
|
61
23
|
|
|
62
24
|
request_id = session_id.dup.freeze
|
|
63
|
-
deletes_current = same_session_id?(
|
|
25
|
+
deletes_current = same_session_id?(session.access_token, request_id)
|
|
64
26
|
error = delete_bound_session_error(binding, request_id)
|
|
65
27
|
current_unchanged = session_unchanged_after_deletion?(deletes_current, error, binding)
|
|
66
28
|
raise Error::SessionChangedError, cause: error unless current_unchanged
|
|
@@ -119,12 +81,14 @@ module Volcano
|
|
|
119
81
|
|
|
120
82
|
def deletion_binding_current?(binding, error)
|
|
121
83
|
active = @client.capture_session_binding
|
|
122
|
-
|
|
84
|
+
return true if active.last && active[1] == binding[1]
|
|
85
|
+
|
|
86
|
+
!error.nil? && rejected_refresh?(binding, active)
|
|
123
87
|
end
|
|
124
88
|
|
|
125
89
|
def same_session_id?(access_token, session_id)
|
|
126
90
|
current_session_id = access_token_session_id(access_token)
|
|
127
|
-
session_id.is_a?(String) && current_session_id&.casecmp?(session_id)
|
|
91
|
+
session_id.is_a?(String) && current_session_id&.casecmp?(session_id) == true
|
|
128
92
|
end
|
|
129
93
|
|
|
130
94
|
def access_token_session_id(access_token)
|
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Revokes the captured server session and clears its local lineage.
|
|
5
|
-
|
|
5
|
+
module AuthSignOut
|
|
6
|
+
include AuthNotificationDispatch
|
|
7
|
+
|
|
6
8
|
def sign_out
|
|
9
|
+
# @type var notifications: Array[Method]
|
|
10
|
+
notifications = []
|
|
7
11
|
binding = @client.capture_session_binding
|
|
8
|
-
|
|
12
|
+
session = binding.last
|
|
13
|
+
return binding[1].wait_for_sign_out unless session
|
|
9
14
|
|
|
10
|
-
notifications = []
|
|
11
15
|
binding[1].sign_out do |preceding, pending|
|
|
12
|
-
sign_out_captured(binding, preceding, pending, notifications)
|
|
16
|
+
sign_out_captured(binding, session, preceding, pending, notifications)
|
|
13
17
|
end
|
|
14
18
|
ensure
|
|
15
|
-
notifications
|
|
19
|
+
run_auth_notifications(notifications)
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
private
|
|
19
23
|
|
|
20
|
-
def sign_out_captured(binding, preceding, pending, notifications)
|
|
24
|
+
def sign_out_captured(binding, session, preceding, pending, notifications)
|
|
21
25
|
generation, owner, = binding
|
|
22
|
-
current, refresh_error = revocation_credentials(
|
|
26
|
+
current, refresh_error = revocation_credentials(owner, session, preceding)
|
|
23
27
|
error = revocation_error(current, owner, pending ? refresh_error : nil, joined: pending)
|
|
24
28
|
cleared = @client.clear_session_if_current?(generation, lineage: owner, notifications: notifications)
|
|
25
29
|
raise Error::SessionChangedError, cause: error unless cleared
|
|
@@ -27,10 +31,13 @@ module Volcano
|
|
|
27
31
|
raise error if error
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def revocation_credentials(
|
|
31
|
-
|
|
34
|
+
def revocation_credentials(owner, session, preceding)
|
|
35
|
+
resolved = owner.result(preceding) || session
|
|
36
|
+
raise TypeError, 'Expected a session from refresh' unless resolved.is_a?(Session)
|
|
37
|
+
|
|
38
|
+
[resolved, nil]
|
|
32
39
|
rescue Error::VolcanoError => e
|
|
33
|
-
[
|
|
40
|
+
[session, e]
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
def logout_response(refresh_token)
|
|
@@ -67,16 +74,17 @@ module Volcano
|
|
|
67
74
|
|
|
68
75
|
def revoke_access_session(session, session_id, refresh_error, joined:)
|
|
69
76
|
error = delete_session_error(session.access_token, session_id)
|
|
70
|
-
|
|
77
|
+
refresh_token = refreshable_revocation_token(error, session)
|
|
78
|
+
return error unless refresh_token
|
|
71
79
|
return refresh_error || error if joined
|
|
72
80
|
|
|
73
|
-
refreshed = parse_refresh_session(Transport.body(refresh_response(
|
|
81
|
+
refreshed = parse_refresh_session(Transport.body(refresh_response(refresh_token), 200))
|
|
74
82
|
SessionCredentials.validate_refresh(session, refreshed)
|
|
75
83
|
delete_session_error(refreshed.access_token, session_id)
|
|
76
84
|
end
|
|
77
85
|
|
|
78
|
-
def
|
|
79
|
-
error&.status == 401
|
|
86
|
+
def refreshable_revocation_token(error, session)
|
|
87
|
+
session.refresh_token if error&.status == 401
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
end
|
data/lib/volcano/auth_state.rb
CHANGED
|
@@ -81,9 +81,10 @@ module Volcano
|
|
|
81
81
|
|
|
82
82
|
def update_user_if_current?(user, generation)
|
|
83
83
|
@mutex.synchronize do
|
|
84
|
-
|
|
84
|
+
current = @session
|
|
85
|
+
return false unless generation == @generation && current
|
|
85
86
|
|
|
86
|
-
@session = SessionCredentials.with_user(
|
|
87
|
+
@session = SessionCredentials.with_user(current, user)
|
|
87
88
|
true
|
|
88
89
|
end
|
|
89
90
|
end
|