volcano-sdk 0.29.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/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 +13 -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 +10 -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
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Queues auth events under the state mutex and invokes subscribers outside it.
|
|
5
5
|
module AuthStateNotifications
|
|
6
|
-
|
|
7
|
-
private_constant :
|
|
6
|
+
CALLBACK_FAILURE = Exception
|
|
7
|
+
private_constant :CALLBACK_FAILURE
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
|
|
@@ -26,12 +26,14 @@ module Volcano
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def drain_notifications
|
|
29
|
+
# @type var failure: Exception?
|
|
29
30
|
failure = nil
|
|
30
31
|
loop do
|
|
31
32
|
notification = next_notification
|
|
32
33
|
break unless notification
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
callback_ids, event, session = notification
|
|
36
|
+
current_failure = notify(callback_ids, event, session)
|
|
35
37
|
failure ||= current_failure
|
|
36
38
|
end
|
|
37
39
|
raise failure if failure
|
|
@@ -48,12 +50,13 @@ module Volcano
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def notify(callback_ids, event, session)
|
|
53
|
+
# @type var failure: Exception?
|
|
51
54
|
failure = nil
|
|
52
55
|
callback_ids.each do |callback_id|
|
|
53
56
|
@mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
|
|
54
57
|
rescue StandardError => e
|
|
55
58
|
Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
|
|
56
|
-
rescue
|
|
59
|
+
rescue CALLBACK_FAILURE => e
|
|
57
60
|
unsubscribe(callback_id)
|
|
58
61
|
failure ||= e
|
|
59
62
|
end
|
data/lib/volcano/session.rb
CHANGED
|
@@ -9,79 +9,6 @@ module Volcano
|
|
|
9
9
|
end
|
|
10
10
|
private_constant :SESSION_USER_CODER
|
|
11
11
|
|
|
12
|
-
# Owns credential snapshots and preserves validated identity during refresh.
|
|
13
|
-
module SessionCredentials
|
|
14
|
-
def self.build(access_token, refresh_token)
|
|
15
|
-
raise ArgumentError, 'refresh_token requires access_token' if access_token.nil? && !refresh_token.nil?
|
|
16
|
-
return if access_token.nil?
|
|
17
|
-
|
|
18
|
-
Session.new(access_token: credential(access_token, :access_token),
|
|
19
|
-
refresh_token: credential(refresh_token, :refresh_token))
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.credential(value, name)
|
|
23
|
-
return if value.nil?
|
|
24
|
-
|
|
25
|
-
raise ArgumentError, "#{name} must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
|
|
26
|
-
|
|
27
|
-
value.dup.freeze
|
|
28
|
-
end
|
|
29
|
-
private_class_method :credential
|
|
30
|
-
|
|
31
|
-
def self.validate_refresh_source(current, verified: false)
|
|
32
|
-
return if verified || session_id(current.access_token)
|
|
33
|
-
|
|
34
|
-
raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.validate_refresh(current, refreshed)
|
|
38
|
-
return unless current
|
|
39
|
-
|
|
40
|
-
validate_refresh_session_id(current, refreshed)
|
|
41
|
-
return unless current.user_id && current.user_id != refreshed&.user_id
|
|
42
|
-
|
|
43
|
-
raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def self.validate_refresh_session_id(current, refreshed)
|
|
47
|
-
expected = session_id(current.access_token)
|
|
48
|
-
return unless expected && expected != session_id(refreshed&.access_token)
|
|
49
|
-
|
|
50
|
-
raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
|
|
51
|
-
end
|
|
52
|
-
private_class_method :validate_refresh_session_id
|
|
53
|
-
|
|
54
|
-
# Untrusted continuity constraint, never authenticated user identity.
|
|
55
|
-
def self.session_id(access_token)
|
|
56
|
-
payload = token_payload(access_token)
|
|
57
|
-
return unless payload.is_a?(Hash)
|
|
58
|
-
|
|
59
|
-
value = payload['session_id']
|
|
60
|
-
value.downcase if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def self.token_payload(access_token)
|
|
64
|
-
return unless access_token.is_a?(String)
|
|
65
|
-
|
|
66
|
-
parts = access_token.split('.')
|
|
67
|
-
return unless parts.length == 3
|
|
68
|
-
|
|
69
|
-
encoded = parts.fetch(1).tr('-_', '+/')
|
|
70
|
-
JSON.parse((encoded + ('=' * (-encoded.length % 4))).unpack1('m0'))
|
|
71
|
-
rescue ArgumentError, JSON::ParserError
|
|
72
|
-
nil
|
|
73
|
-
end
|
|
74
|
-
private_class_method :token_payload
|
|
75
|
-
|
|
76
|
-
def self.with_user(session, user)
|
|
77
|
-
user_id = (session.user_id || user.fetch('id')).dup.freeze
|
|
78
|
-
raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
|
|
79
|
-
|
|
80
|
-
Session.new(**session.to_h, user_id: user_id, user: user)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
private_constant :SessionCredentials
|
|
84
|
-
|
|
85
12
|
# Local credentials; refresh credentials and user identity may be unknown.
|
|
86
13
|
Session = Data.define(:access_token, :refresh_token, :user_id, :user) do
|
|
87
14
|
def initialize(access_token:, refresh_token: nil, user_id: nil, user: nil)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'session'
|
|
6
|
+
|
|
7
|
+
module Volcano
|
|
8
|
+
# Owns credential snapshots and preserves validated identity during refresh.
|
|
9
|
+
module SessionCredentials
|
|
10
|
+
def self.build(access_token, refresh_token)
|
|
11
|
+
raise ArgumentError, 'refresh_token requires access_token' if access_token.nil? && !refresh_token.nil?
|
|
12
|
+
return if access_token.nil?
|
|
13
|
+
|
|
14
|
+
Session.new(access_token: credential(access_token, :access_token),
|
|
15
|
+
refresh_token: refresh_token.nil? ? nil : credential(refresh_token, :refresh_token))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.credential(value, name)
|
|
19
|
+
raise ArgumentError, "#{name} must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
|
|
20
|
+
|
|
21
|
+
value.dup.freeze
|
|
22
|
+
end
|
|
23
|
+
private_class_method :credential
|
|
24
|
+
|
|
25
|
+
def self.validate_refresh_source(current, verified: false)
|
|
26
|
+
return if verified || session_id(current.access_token)
|
|
27
|
+
|
|
28
|
+
raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.validate_refresh(current, refreshed)
|
|
32
|
+
return unless current
|
|
33
|
+
|
|
34
|
+
validate_refresh_session_id(current, refreshed)
|
|
35
|
+
return unless current.user_id && current.user_id != refreshed&.user_id
|
|
36
|
+
|
|
37
|
+
raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.validate_refresh_session_id(current, refreshed)
|
|
41
|
+
expected = session_id(current.access_token)
|
|
42
|
+
return unless expected && expected != session_id(refreshed&.access_token)
|
|
43
|
+
|
|
44
|
+
raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
|
|
45
|
+
end
|
|
46
|
+
private_class_method :validate_refresh_session_id
|
|
47
|
+
|
|
48
|
+
# Untrusted continuity constraint, never authenticated user identity.
|
|
49
|
+
def self.session_id(access_token)
|
|
50
|
+
payload = token_payload(access_token)
|
|
51
|
+
return unless payload.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
value = payload['session_id']
|
|
54
|
+
value.downcase if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.token_payload(access_token)
|
|
58
|
+
return unless access_token.is_a?(String)
|
|
59
|
+
|
|
60
|
+
parts = access_token.split('.')
|
|
61
|
+
return unless parts.length == 3
|
|
62
|
+
|
|
63
|
+
encoded = parts.fetch(1).tr('-_', '+/')
|
|
64
|
+
JSON.parse(Base64.strict_decode64(encoded + ('=' * (-encoded.length % 4))))
|
|
65
|
+
rescue ArgumentError, JSON::ParserError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
private_class_method :token_payload
|
|
69
|
+
|
|
70
|
+
def self.with_user(session, user)
|
|
71
|
+
user_id = session.user_id || user.fetch('id')
|
|
72
|
+
raise Error::AuthenticationError, 'Profile has no valid user identifier' unless user_id.is_a?(String)
|
|
73
|
+
|
|
74
|
+
user_id = user_id.dup.freeze
|
|
75
|
+
raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
|
|
76
|
+
|
|
77
|
+
Session.new(access_token: session.access_token, refresh_token: session.refresh_token,
|
|
78
|
+
user_id: user_id, user: user)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
private_constant :SessionCredentials
|
|
82
|
+
end
|
|
@@ -42,7 +42,9 @@ module Volcano
|
|
|
42
42
|
def refresh(&)
|
|
43
43
|
operation, owner = @mutex.synchronize do
|
|
44
44
|
raise Error::SessionChangedError if @sign_out || @locally_cleared
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
current = @refresh
|
|
47
|
+
next [current, false] if current && !current.done
|
|
46
48
|
|
|
47
49
|
@refresh = Outcome.new(false)
|
|
48
50
|
[@refresh, true]
|
|
@@ -51,11 +53,7 @@ module Volcano
|
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def sign_out
|
|
54
|
-
operation, owner, preceding, pending = @mutex.synchronize
|
|
55
|
-
first = @sign_out.nil?
|
|
56
|
-
@sign_out ||= Outcome.new(false)
|
|
57
|
-
[@sign_out, first, @refresh, @refresh && !@refresh.done]
|
|
58
|
-
end
|
|
56
|
+
operation, owner, preceding, pending = @mutex.synchronize { claim_sign_out }
|
|
59
57
|
execute(operation, owner) do
|
|
60
58
|
yield(preceding, pending)
|
|
61
59
|
ensure
|
|
@@ -79,10 +77,19 @@ module Volcano
|
|
|
79
77
|
|
|
80
78
|
private
|
|
81
79
|
|
|
80
|
+
def claim_sign_out
|
|
81
|
+
first = @sign_out.nil? == true
|
|
82
|
+
operation = @sign_out ||= Outcome.new(false)
|
|
83
|
+
current = @refresh
|
|
84
|
+
[operation, first, current, current && !current.done]
|
|
85
|
+
end
|
|
86
|
+
|
|
82
87
|
def execute(operation, owner)
|
|
83
88
|
return result(operation) unless owner
|
|
84
89
|
|
|
85
90
|
completed = false
|
|
91
|
+
# @type var value: Object?
|
|
92
|
+
value = nil
|
|
86
93
|
begin
|
|
87
94
|
value = yield
|
|
88
95
|
completed = true
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Maps the internal wire response to immutable public session values.
|
|
5
|
+
module SessionPageMapping
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def session_page(body)
|
|
9
|
+
raise TypeError, 'Expected a complete session page' unless body.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
sessions = body.fetch('sessions')
|
|
12
|
+
raise TypeError, 'Expected a complete session page' unless sessions.is_a?(Array)
|
|
13
|
+
|
|
14
|
+
SessionPage.new(
|
|
15
|
+
sessions: sessions.map { |attributes| auth_session(attributes) },
|
|
16
|
+
total: required_integer(body, 'total'), page: required_integer(body, 'page'),
|
|
17
|
+
limit: required_integer(body, 'limit'), total_pages: required_integer(body, 'total_pages')
|
|
18
|
+
)
|
|
19
|
+
rescue KeyError
|
|
20
|
+
raise TypeError, 'Expected a complete session page'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def auth_session(attributes)
|
|
24
|
+
raise TypeError, 'Expected a complete authentication session' unless attributes.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
build_auth_session(attributes)
|
|
27
|
+
rescue KeyError
|
|
28
|
+
raise TypeError, 'Expected a complete authentication session'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_auth_session(attributes)
|
|
32
|
+
AuthSession.new(
|
|
33
|
+
id: required_string(attributes, 'id'), user_id: required_string(attributes, 'user_id'),
|
|
34
|
+
provider: required_string(attributes, 'provider'), expires_at: required_time(attributes, 'expires_at'),
|
|
35
|
+
is_active: required_boolean(attributes, 'is_active'), is_current: required_boolean(attributes, 'is_current'),
|
|
36
|
+
user_agent: optional_string(attributes, 'user_agent'), ip_address: optional_string(attributes, 'ip_address'),
|
|
37
|
+
last_ip_address: optional_string(attributes, 'last_ip_address'),
|
|
38
|
+
last_activity_at: optional_time(attributes, 'last_activity_at'),
|
|
39
|
+
session_started_at: optional_time(attributes, 'session_started_at'),
|
|
40
|
+
created_at: optional_time(attributes, 'created_at'), updated_at: optional_time(attributes, 'updated_at')
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def required_integer(body, name)
|
|
45
|
+
value = body.fetch(name)
|
|
46
|
+
raise TypeError, 'Expected a complete session page' unless value.is_a?(Integer)
|
|
47
|
+
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def required_string(body, name)
|
|
52
|
+
value = body.fetch(name)
|
|
53
|
+
raise TypeError, 'Expected a complete authentication session' unless value.is_a?(String)
|
|
54
|
+
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def required_time(body, name)
|
|
59
|
+
value = body.fetch(name)
|
|
60
|
+
raise TypeError, 'Expected a complete authentication session' unless value.is_a?(Time)
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def required_boolean(body, name)
|
|
66
|
+
value = body.fetch(name)
|
|
67
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
68
|
+
raise TypeError, 'Expected a complete authentication session'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def optional_string(body, name)
|
|
75
|
+
value = body[name]
|
|
76
|
+
raise TypeError, 'Expected a complete authentication session' unless value.nil? || value.is_a?(String)
|
|
77
|
+
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def optional_time(body, name)
|
|
82
|
+
value = body[name]
|
|
83
|
+
raise TypeError, 'Expected a complete authentication session' unless value.nil? || value.is_a?(Time)
|
|
84
|
+
|
|
85
|
+
value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
private_constant :SessionPageMapping
|
|
89
|
+
end
|
data/lib/volcano/version.rb
CHANGED
data/lib/volcano.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative 'volcano/connection_string'
|
|
|
5
5
|
require_relative 'volcano/models'
|
|
6
6
|
require_relative 'volcano/user'
|
|
7
7
|
require_relative 'volcano/session'
|
|
8
|
+
require_relative 'volcano/session_credentials'
|
|
8
9
|
require_relative 'volcano/function_response'
|
|
9
10
|
require_relative 'volcano/durable_models'
|
|
10
11
|
require_relative 'volcano/log_responses'
|
|
@@ -16,16 +17,21 @@ require_relative 'volcano/errors'
|
|
|
16
17
|
require_relative 'volcano/redaction'
|
|
17
18
|
require_relative 'volcano/transport'
|
|
18
19
|
require_relative 'volcano/generated_transport'
|
|
19
|
-
require_relative 'volcano/
|
|
20
|
+
require_relative 'volcano/auth_notification_dispatch'
|
|
21
|
+
require_relative 'volcano/auth_session_binding'
|
|
20
22
|
require_relative 'volcano/auth_sign_out'
|
|
21
23
|
require_relative 'volcano/auth_refresh'
|
|
24
|
+
require_relative 'volcano/auth_profile_fields'
|
|
25
|
+
require_relative 'volcano/auth_get_user'
|
|
26
|
+
require_relative 'volcano/auth_update_user'
|
|
27
|
+
require_relative 'volcano/auth_email_change'
|
|
28
|
+
require_relative 'volcano/auth_sessions'
|
|
29
|
+
require_relative 'volcano/auth'
|
|
22
30
|
require_relative 'volcano/auth_sign_up'
|
|
23
31
|
require_relative 'volcano/sign_up_result'
|
|
24
32
|
require_relative 'volcano/auth_anonymous'
|
|
25
33
|
require_relative 'volcano/auth_confirm_email'
|
|
26
34
|
require_relative 'volcano/auth_resend_confirmation'
|
|
27
|
-
require_relative 'volcano/auth_email_change'
|
|
28
|
-
require_relative 'volcano/auth_sessions'
|
|
29
35
|
require_relative 'volcano/auth_oauth'
|
|
30
36
|
require_relative 'volcano/auth_oauth_sign_in'
|
|
31
37
|
require_relative 'volcano/auth_hosted'
|
|
@@ -33,8 +39,6 @@ require_relative 'volcano/auth_oauth_token'
|
|
|
33
39
|
require_relative 'volcano/auth_oauth_api'
|
|
34
40
|
require_relative 'volcano/auth_reset_password_for_email'
|
|
35
41
|
require_relative 'volcano/auth_reset_password'
|
|
36
|
-
require_relative 'volcano/auth_get_user'
|
|
37
|
-
require_relative 'volcano/auth_update_user'
|
|
38
42
|
require_relative 'volcano/database'
|
|
39
43
|
require_relative 'volcano/function_resolution'
|
|
40
44
|
require_relative 'volcano/functions'
|
|
@@ -58,8 +62,10 @@ require_relative 'volcano/client'
|
|
|
58
62
|
|
|
59
63
|
# Public namespace for the Volcano Ruby SDK.
|
|
60
64
|
module Volcano
|
|
61
|
-
private_constant :
|
|
62
|
-
:
|
|
65
|
+
private_constant :AuthRefresh, :AuthSignOut, :AuthProfile, :AuthProfileFields, :AuthUpdateUser, :AuthEmailChange,
|
|
66
|
+
:AuthSessions,
|
|
67
|
+
:GeneratedTransport, :LockAutoRenewal, :LockLeaseClock, :LockRenewer,
|
|
68
|
+
:LockSession, :LockResponse, :Redaction, :SessionOperations
|
|
63
69
|
|
|
64
70
|
Realtime.private_constant :Protocol, :ProtocolDispatch, :ProtocolPublicationDispatch, :ProtocolRecovery,
|
|
65
71
|
:ProtocolRecoveryDispatch, :ProtocolRecoveryPosition
|
data/sig/auth_oauth.rbs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Volcano
|
|
2
|
+
type oauth_api_input = session_input
|
|
3
|
+
|
|
4
|
+
class Auth
|
|
5
|
+
def list_linked_oauth_providers: () -> Array[LinkedOAuthProvider]
|
|
6
|
+
def link_oauth_provider: (String provider) -> String
|
|
7
|
+
def unlink_oauth_provider: (String provider) -> nil
|
|
8
|
+
def get_oauth_provider_token: (String provider) -> OAuthProviderTokenStatus
|
|
9
|
+
def refresh_oauth_provider_token: (String provider) -> OAuthProviderTokenStatus
|
|
10
|
+
def call_oauth_api: (String provider, endpoint: String, ?method: String, ?body: oauth_api_input) -> Object?
|
|
11
|
+
def sign_in_with_oauth: (String provider, redirect_to: String, state: String) -> String
|
|
12
|
+
def exchange_oauth_code: (code: String, redirect_to: String, state: String, expected_state: String) -> Session
|
|
13
|
+
def get_hosted_auth_url: (project_id: String, state: String, ?action: String) -> String
|
|
14
|
+
def adopt_hosted_auth_session: (Session session, state: String, expected_state: String) -> Session
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Volcano
|
|
2
|
+
class Auth
|
|
3
|
+
def user: () -> User
|
|
4
|
+
def get_user: () -> User
|
|
5
|
+
def update_user: (?password: String?, ?metadata: session_user_input?) -> User
|
|
6
|
+
def request_email_change: (new_email: String) -> EmailChangeResult
|
|
7
|
+
def cancel_email_change: () -> nil
|
|
8
|
+
def confirm_email_change: (token: String) -> User
|
|
9
|
+
end
|
|
10
|
+
end
|
data/sig/session.rbs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Volcano
|
|
2
2
|
type json_value = nil | bool | Integer | Float | String | Array[json_value] | Hash[String, json_value]
|
|
3
3
|
type session_input = nil | bool | Integer | Float | String | Symbol | Time | Array[session_input] | Hash[String | Symbol, session_input]
|
|
4
|
-
type session_user_input = Hash[String, session_input] | Hash[Symbol, session_input] | Hash[String | Symbol, session_input]
|
|
4
|
+
type session_user_input = Hash[String, json_value] | Hash[String, session_input] | Hash[Symbol, session_input] | Hash[String | Symbol, session_input]
|
|
5
5
|
|
|
6
6
|
class Session < Data
|
|
7
7
|
attr_reader access_token: String
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: volcano-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kong
|
|
@@ -136,14 +136,17 @@ files:
|
|
|
136
136
|
- lib/volcano/auth_email_change.rb
|
|
137
137
|
- lib/volcano/auth_get_user.rb
|
|
138
138
|
- lib/volcano/auth_hosted.rb
|
|
139
|
+
- lib/volcano/auth_notification_dispatch.rb
|
|
139
140
|
- lib/volcano/auth_oauth.rb
|
|
140
141
|
- lib/volcano/auth_oauth_api.rb
|
|
141
142
|
- lib/volcano/auth_oauth_sign_in.rb
|
|
142
143
|
- lib/volcano/auth_oauth_token.rb
|
|
144
|
+
- lib/volcano/auth_profile_fields.rb
|
|
143
145
|
- lib/volcano/auth_refresh.rb
|
|
144
146
|
- lib/volcano/auth_resend_confirmation.rb
|
|
145
147
|
- lib/volcano/auth_reset_password.rb
|
|
146
148
|
- lib/volcano/auth_reset_password_for_email.rb
|
|
149
|
+
- lib/volcano/auth_session_binding.rb
|
|
147
150
|
- lib/volcano/auth_sessions.rb
|
|
148
151
|
- lib/volcano/auth_sign_out.rb
|
|
149
152
|
- lib/volcano/auth_sign_up.rb
|
|
@@ -592,7 +595,9 @@ files:
|
|
|
592
595
|
- lib/volcano/realtime/reconnect.rb
|
|
593
596
|
- lib/volcano/redaction.rb
|
|
594
597
|
- lib/volcano/session.rb
|
|
598
|
+
- lib/volcano/session_credentials.rb
|
|
595
599
|
- lib/volcano/session_operations.rb
|
|
600
|
+
- lib/volcano/session_page_mapping.rb
|
|
596
601
|
- lib/volcano/sign_up_result.rb
|
|
597
602
|
- lib/volcano/storage.rb
|
|
598
603
|
- lib/volcano/storage_models.rb
|
|
@@ -604,7 +609,11 @@ files:
|
|
|
604
609
|
- lib/volcano/transport.rb
|
|
605
610
|
- lib/volcano/user.rb
|
|
606
611
|
- lib/volcano/version.rb
|
|
612
|
+
- sig/auth_oauth.rbs
|
|
613
|
+
- sig/auth_profile.rbs
|
|
607
614
|
- sig/auth_session_records.rbs
|
|
615
|
+
- sig/auth_sessions.rbs
|
|
616
|
+
- sig/auth_subscription.rbs
|
|
608
617
|
- sig/connection_string.rbs
|
|
609
618
|
- sig/durable_records.rbs
|
|
610
619
|
- sig/email_change_result.rbs
|