workos 5.6.0 → 5.8.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/.rubocop.yml +1 -1
- data/Gemfile.lock +7 -1
- data/lib/workos/authentication_response.rb +25 -4
- data/lib/workos/profile.rb +3 -1
- data/lib/workos/refresh_authentication_response.rb +25 -2
- data/lib/workos/session.rb +183 -0
- data/lib/workos/user_management.rb +34 -4
- data/lib/workos/version.rb +1 -1
- data/lib/workos.rb +1 -0
- data/spec/lib/workos/session_spec.rb +214 -0
- data/spec/lib/workos/sso_spec.rb +6 -0
- data/spec/lib/workos/user_management_spec.rb +1 -0
- data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +1 -1
- data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml +79 -78
- data/spec/support/profile.txt +1 -1
- data/workos.gemspec +3 -0
- metadata +33 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0afe2a59cbc6559cee74fe894ad0ed632b6788955377949f87c641a3550fff92
|
4
|
+
data.tar.gz: 1725783e695045041a65334679db26c2fb396f1e79a3c4d18d7491d91d479756
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 626069aef80ea5cf1b3254679fb7f7c9054fe7f908c464521e42c566186c21170f5a3187e98c59d6f1f209d107e53995facb58763965042ce257d2e4cd8f5d63
|
7
|
+
data.tar.gz: 0f941008d87d91b19bbd5829cf79580df7cabb9e3d37891aa3016fca9e4af844ecd8a30f3b4e6e567d6a47ece9249b6e6da02eda157928bd2ee8c5d45c6404d2
|
data/.rubocop.yml
CHANGED
@@ -12,7 +12,7 @@ Layout/LineLength:
|
|
12
12
|
- 'VCR\.use_cassette'
|
13
13
|
- '(\A|\s)/.*?/'
|
14
14
|
Metrics/BlockLength:
|
15
|
-
ExcludedMethods: ['describe', 'context', 'before']
|
15
|
+
ExcludedMethods: ['describe', 'context', 'before', 'it']
|
16
16
|
Metrics/MethodLength:
|
17
17
|
Max: 30
|
18
18
|
Metrics/ModuleLength:
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
workos (5.
|
4
|
+
workos (5.8.0)
|
5
|
+
encryptor (~> 3.0)
|
6
|
+
jwt (~> 2.8)
|
5
7
|
|
6
8
|
GEM
|
7
9
|
remote: https://rubygems.org/
|
@@ -9,12 +11,16 @@ GEM
|
|
9
11
|
addressable (2.8.6)
|
10
12
|
public_suffix (>= 2.0.2, < 6.0)
|
11
13
|
ast (2.4.2)
|
14
|
+
base64 (0.2.0)
|
12
15
|
bigdecimal (3.1.7)
|
13
16
|
crack (1.0.0)
|
14
17
|
bigdecimal
|
15
18
|
rexml
|
16
19
|
diff-lcs (1.5.1)
|
20
|
+
encryptor (3.0.0)
|
17
21
|
hashdiff (1.1.0)
|
22
|
+
jwt (2.8.2)
|
23
|
+
base64
|
18
24
|
parallel (1.24.0)
|
19
25
|
parser (3.3.0.5)
|
20
26
|
ast (~> 2.4.1)
|
@@ -6,10 +6,19 @@ module WorkOS
|
|
6
6
|
class AuthenticationResponse
|
7
7
|
include HashProvider
|
8
8
|
|
9
|
-
attr_accessor :user,
|
9
|
+
attr_accessor :user,
|
10
|
+
:organization_id,
|
11
|
+
:impersonator,
|
12
|
+
:access_token,
|
13
|
+
:refresh_token,
|
14
|
+
:authentication_method,
|
15
|
+
:sealed_session
|
10
16
|
|
11
|
-
|
17
|
+
# rubocop:disable Metrics/AbcSize
|
18
|
+
def initialize(authentication_response_json, session = nil)
|
12
19
|
json = JSON.parse(authentication_response_json, symbolize_names: true)
|
20
|
+
@access_token = json[:access_token]
|
21
|
+
@refresh_token = json[:refresh_token]
|
13
22
|
@user = WorkOS::User.new(json[:user].to_json)
|
14
23
|
@organization_id = json[:organization_id]
|
15
24
|
@impersonator =
|
@@ -17,9 +26,19 @@ module WorkOS
|
|
17
26
|
Impersonator.new(email: impersonator_json[:email],
|
18
27
|
reason: impersonator_json[:reason],)
|
19
28
|
end
|
20
|
-
@
|
21
|
-
@
|
29
|
+
@authentication_method = json[:authentication_method]
|
30
|
+
@sealed_session =
|
31
|
+
if session && session[:seal_session]
|
32
|
+
WorkOS::Session.seal_data({
|
33
|
+
access_token: access_token,
|
34
|
+
refresh_token: refresh_token,
|
35
|
+
user: user.to_json,
|
36
|
+
organization_id: organization_id,
|
37
|
+
impersonator: impersonator.to_json,
|
38
|
+
}, session[:cookie_password],)
|
39
|
+
end
|
22
40
|
end
|
41
|
+
# rubocop:enable Metrics/AbcSize
|
23
42
|
|
24
43
|
def to_json(*)
|
25
44
|
{
|
@@ -28,6 +47,8 @@ module WorkOS
|
|
28
47
|
impersonator: impersonator.to_json,
|
29
48
|
access_token: access_token,
|
30
49
|
refresh_token: refresh_token,
|
50
|
+
authentication_method: authentication_method,
|
51
|
+
sealed_session: sealed_session,
|
31
52
|
}
|
32
53
|
end
|
33
54
|
end
|
data/lib/workos/profile.rb
CHANGED
@@ -9,7 +9,7 @@ module WorkOS
|
|
9
9
|
class Profile
|
10
10
|
include HashProvider
|
11
11
|
|
12
|
-
attr_accessor :id, :email, :first_name, :last_name, :groups, :organization_id,
|
12
|
+
attr_accessor :id, :email, :first_name, :last_name, :role, :groups, :organization_id,
|
13
13
|
:connection_id, :connection_type, :idp_id, :raw_attributes
|
14
14
|
|
15
15
|
def initialize(profile_json)
|
@@ -19,6 +19,7 @@ module WorkOS
|
|
19
19
|
@email = hash[:email]
|
20
20
|
@first_name = hash[:first_name]
|
21
21
|
@last_name = hash[:last_name]
|
22
|
+
@role = hash[:role]
|
22
23
|
@groups = hash[:groups]
|
23
24
|
@organization_id = hash[:organization_id]
|
24
25
|
@connection_id = hash[:connection_id]
|
@@ -37,6 +38,7 @@ module WorkOS
|
|
37
38
|
email: email,
|
38
39
|
first_name: first_name,
|
39
40
|
last_name: last_name,
|
41
|
+
role: role,
|
40
42
|
groups: groups,
|
41
43
|
organization_id: organization_id,
|
42
44
|
connection_id: connection_id,
|
@@ -6,18 +6,41 @@ module WorkOS
|
|
6
6
|
class RefreshAuthenticationResponse
|
7
7
|
include HashProvider
|
8
8
|
|
9
|
-
attr_accessor :access_token, :refresh_token
|
9
|
+
attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token, :sealed_session
|
10
10
|
|
11
|
-
|
11
|
+
# rubocop:disable Metrics/AbcSize
|
12
|
+
def initialize(authentication_response_json, session = nil)
|
12
13
|
json = JSON.parse(authentication_response_json, symbolize_names: true)
|
13
14
|
@access_token = json[:access_token]
|
14
15
|
@refresh_token = json[:refresh_token]
|
16
|
+
@user = WorkOS::User.new(json[:user].to_json)
|
17
|
+
@organization_id = json[:organization_id]
|
18
|
+
@impersonator =
|
19
|
+
if (impersonator_json = json[:impersonator])
|
20
|
+
Impersonator.new(email: impersonator_json[:email],
|
21
|
+
reason: impersonator_json[:reason],)
|
22
|
+
end
|
23
|
+
@sealed_session =
|
24
|
+
if session && session[:seal_session]
|
25
|
+
WorkOS::Session.seal_data({
|
26
|
+
access_token: access_token,
|
27
|
+
refresh_token: refresh_token,
|
28
|
+
user: user.to_json,
|
29
|
+
organization_id: organization_id,
|
30
|
+
impersonator: impersonator.to_json,
|
31
|
+
}, session[:cookie_password],)
|
32
|
+
end
|
15
33
|
end
|
34
|
+
# rubocop:enable Metrics/AbcSize
|
16
35
|
|
17
36
|
def to_json(*)
|
18
37
|
{
|
38
|
+
user: user.to_json,
|
39
|
+
organization_id: organization_id,
|
40
|
+
impersonator: impersonator.to_json,
|
19
41
|
access_token: access_token,
|
20
42
|
refresh_token: refresh_token,
|
43
|
+
sealed_session: sealed_session,
|
21
44
|
}
|
22
45
|
end
|
23
46
|
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'encryptor'
|
7
|
+
require 'securerandom'
|
8
|
+
require 'json'
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
module WorkOS
|
12
|
+
# The Session class provides helper methods for working with WorkOS sessions
|
13
|
+
# This class is not meant to be instantiated in a user space, and is instantiated internally but exposed.
|
14
|
+
# rubocop:disable Metrics/ClassLength
|
15
|
+
class Session
|
16
|
+
attr_accessor :jwks, :jwks_algorithms, :user_management, :cookie_password, :session_data, :client_id
|
17
|
+
|
18
|
+
def initialize(user_management:, client_id:, session_data:, cookie_password:)
|
19
|
+
raise ArgumentError, 'cookiePassword is required' if cookie_password.nil? || cookie_password.empty?
|
20
|
+
|
21
|
+
@user_management = user_management
|
22
|
+
@cookie_password = cookie_password
|
23
|
+
@session_data = session_data
|
24
|
+
@client_id = client_id
|
25
|
+
|
26
|
+
@jwks = create_remote_jwk_set(URI(@user_management.get_jwks_url(client_id)))
|
27
|
+
@jwks_algorithms = @jwks.map { |key| key[:alg] }.compact.uniq
|
28
|
+
end
|
29
|
+
|
30
|
+
# Authenticates the user based on the session data
|
31
|
+
# @return [Hash] A hash containing the authentication response and a reason if the authentication failed
|
32
|
+
def authenticate
|
33
|
+
return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil?
|
34
|
+
|
35
|
+
begin
|
36
|
+
session = Session.unseal_data(@session_data, @cookie_password)
|
37
|
+
rescue StandardError
|
38
|
+
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
|
39
|
+
end
|
40
|
+
|
41
|
+
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:access_token]
|
42
|
+
return { authenticated: false, reason: 'INVALID_JWT' } unless is_valid_jwt(session[:access_token])
|
43
|
+
|
44
|
+
decoded = JWT.decode(session[:access_token], nil, true, algorithms: @jwks_algorithms, jwks: @jwks).first
|
45
|
+
|
46
|
+
{
|
47
|
+
authenticated: true,
|
48
|
+
session_id: decoded['sid'],
|
49
|
+
organization_id: decoded['org_id'],
|
50
|
+
role: decoded['role'],
|
51
|
+
permissions: decoded['permissions'],
|
52
|
+
user: session[:user],
|
53
|
+
impersonator: session[:impersonator],
|
54
|
+
reason: nil,
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Refreshes the session data using the refresh token stored in the session data
|
59
|
+
# @param options [Hash] Options for refreshing the session
|
60
|
+
# @option options [String] :cookie_password The password to use for unsealing the session data
|
61
|
+
# @option options [String] :organization_id The organization ID to use for refreshing the session
|
62
|
+
# @return [Hash] A hash containing a new sealed session, the authentication response,
|
63
|
+
# and a reason if the refresh failed
|
64
|
+
# rubocop:disable Metrics/AbcSize
|
65
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
66
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
67
|
+
def refresh(options = nil)
|
68
|
+
cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]
|
69
|
+
|
70
|
+
begin
|
71
|
+
session = Session.unseal_data(@session_data, cookie_password)
|
72
|
+
rescue StandardError
|
73
|
+
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
|
74
|
+
end
|
75
|
+
|
76
|
+
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:refresh_token] && session[:user]
|
77
|
+
|
78
|
+
begin
|
79
|
+
auth_response = @user_management.authenticate_with_refresh_token(
|
80
|
+
client_id: @client_id,
|
81
|
+
refresh_token: session[:refresh_token],
|
82
|
+
organization_id: options.nil? || options[:organization_id].nil? ? nil : options[:organization_id],
|
83
|
+
session: { seal_session: true, cookie_password: cookie_password },
|
84
|
+
)
|
85
|
+
|
86
|
+
@session_data = auth_response.sealed_session
|
87
|
+
@cookie_password = cookie_password
|
88
|
+
|
89
|
+
{
|
90
|
+
authenticated: true,
|
91
|
+
sealed_session: auth_response.sealed_session,
|
92
|
+
session: auth_response,
|
93
|
+
reason: nil,
|
94
|
+
}
|
95
|
+
rescue StandardError => e
|
96
|
+
{ authenticated: false, reason: e.message }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
# rubocop:enable Metrics/AbcSize
|
100
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
101
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
102
|
+
|
103
|
+
# Returns a URL to redirect the user to for logging out
|
104
|
+
# @return [String] The URL to redirect the user to for logging out
|
105
|
+
# rubocop:disable Naming/AccessorMethodName
|
106
|
+
def get_logout_url
|
107
|
+
auth_response = authenticate
|
108
|
+
|
109
|
+
unless auth_response[:authenticated]
|
110
|
+
raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}"
|
111
|
+
end
|
112
|
+
|
113
|
+
@user_management.get_logout_url(session_id: auth_response[:session_id])
|
114
|
+
end
|
115
|
+
# rubocop:enable Naming/AccessorMethodName
|
116
|
+
|
117
|
+
# Encrypts and seals data using AES-256-GCM
|
118
|
+
# @param data [Hash] The data to seal
|
119
|
+
# @param key [String] The key to use for encryption
|
120
|
+
# @return [String] The sealed data
|
121
|
+
def self.seal_data(data, key)
|
122
|
+
iv = SecureRandom.random_bytes(12)
|
123
|
+
|
124
|
+
encrypted_data = Encryptor.encrypt(
|
125
|
+
value: JSON.generate(data),
|
126
|
+
key: key,
|
127
|
+
iv: iv,
|
128
|
+
algorithm: 'aes-256-gcm',
|
129
|
+
)
|
130
|
+
Base64.encode64(iv + encrypted_data) # Combine IV with encrypted data and encode as base64
|
131
|
+
end
|
132
|
+
|
133
|
+
# Decrypts and unseals data using AES-256-GCM
|
134
|
+
# @param sealed_data [String] The sealed data to unseal
|
135
|
+
# @param key [String] The key to use for decryption
|
136
|
+
# @return [Hash] The unsealed data
|
137
|
+
def self.unseal_data(sealed_data, key)
|
138
|
+
decoded_data = Base64.decode64(sealed_data)
|
139
|
+
iv = decoded_data[0..11] # Extract the IV (first 12 bytes)
|
140
|
+
encrypted_data = decoded_data[12..-1] # Extract the encrypted data
|
141
|
+
|
142
|
+
decrypted_data = Encryptor.decrypt(
|
143
|
+
value: encrypted_data,
|
144
|
+
key: key,
|
145
|
+
iv: iv,
|
146
|
+
algorithm: 'aes-256-gcm',
|
147
|
+
)
|
148
|
+
|
149
|
+
JSON.parse(decrypted_data, symbolize_names: true) # Parse the decrypted JSON string back to original data
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Creates a JWKS set from a remote JWKS URL
|
155
|
+
# @param uri [URI] The URI of the JWKS
|
156
|
+
# @return [JWT::JWK::Set] The JWKS set
|
157
|
+
def create_remote_jwk_set(uri)
|
158
|
+
# Fetch the JWKS from the remote URL
|
159
|
+
response = Net::HTTP.get(uri)
|
160
|
+
|
161
|
+
jwks_hash = JSON.parse(response)
|
162
|
+
jwks = JWT::JWK::Set.new(jwks_hash)
|
163
|
+
|
164
|
+
# filter jwks so it only returns the keys where 'use' is equal to 'sig'
|
165
|
+
jwks.keys.select! { |key| key[:use] == 'sig' }
|
166
|
+
|
167
|
+
jwks
|
168
|
+
end
|
169
|
+
|
170
|
+
# Validates a JWT token using the JWKS set
|
171
|
+
# @param token [String] The JWT token to validate
|
172
|
+
# @return [Boolean] True if the token is valid, false otherwise
|
173
|
+
# rubocop:disable Naming/PredicateName
|
174
|
+
def is_valid_jwt(token)
|
175
|
+
JWT.decode(token, nil, true, algorithms: @jwks_algorithms, jwks: @jwks)
|
176
|
+
true
|
177
|
+
rescue StandardError
|
178
|
+
false
|
179
|
+
end
|
180
|
+
# rubocop:enable Naming/PredicateName
|
181
|
+
end
|
182
|
+
# rubocop:enable Metrics/ClassLength
|
183
|
+
end
|
@@ -37,6 +37,22 @@ module WorkOS
|
|
37
37
|
PROVIDERS = WorkOS::UserManagement::Types::Provider::ALL
|
38
38
|
AUTH_FACTOR_TYPES = WorkOS::UserManagement::Types::AuthFactorType::ALL
|
39
39
|
|
40
|
+
# Load a sealed session
|
41
|
+
#
|
42
|
+
# @param [String] client_id The WorkOS client ID for the environment
|
43
|
+
# @param [String] session_data The sealed session data
|
44
|
+
# @param [String] cookie_password The password used to seal the session
|
45
|
+
#
|
46
|
+
# @return WorkOS::Session
|
47
|
+
def load_sealed_session(client_id:, session_data:, cookie_password:)
|
48
|
+
WorkOS::Session.new(
|
49
|
+
user_management: self,
|
50
|
+
client_id: client_id,
|
51
|
+
session_data: session_data,
|
52
|
+
cookie_password: cookie_password,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
40
56
|
# Generate an OAuth 2.0 authorization URL that automatically directs a user
|
41
57
|
# to their Identity Provider.
|
42
58
|
#
|
@@ -289,14 +305,21 @@ module WorkOS
|
|
289
305
|
# @param [String] client_id The WorkOS client ID for the environment
|
290
306
|
# @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
|
291
307
|
# @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
|
308
|
+
# @param [Hash] session An optional hash that determines whether the session should be sealed and
|
309
|
+
# the optional cookie password.
|
292
310
|
#
|
293
311
|
# @return WorkOS::AuthenticationResponse
|
294
312
|
def authenticate_with_code(
|
295
313
|
code:,
|
296
314
|
client_id:,
|
297
315
|
ip_address: nil,
|
298
|
-
user_agent: nil
|
316
|
+
user_agent: nil,
|
317
|
+
session: nil
|
299
318
|
)
|
319
|
+
if session && (session[:seal_session] == true) && session[:cookie_password].nil?
|
320
|
+
raise ArgumentError, 'cookie_password is required when sealing session'
|
321
|
+
end
|
322
|
+
|
300
323
|
response = execute_request(
|
301
324
|
request: post_request(
|
302
325
|
path: '/user_management/authenticate',
|
@@ -311,7 +334,7 @@ module WorkOS
|
|
311
334
|
),
|
312
335
|
)
|
313
336
|
|
314
|
-
WorkOS::AuthenticationResponse.new(response.body)
|
337
|
+
WorkOS::AuthenticationResponse.new(response.body, session)
|
315
338
|
end
|
316
339
|
|
317
340
|
# Authenticate a user using a refresh token.
|
@@ -321,6 +344,8 @@ module WorkOS
|
|
321
344
|
# @param [String] organization_id The organization to issue the new access token for. (Optional)
|
322
345
|
# @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
|
323
346
|
# @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
|
347
|
+
# @param [Hash] session An optional hash that determines whether the session should be sealed and
|
348
|
+
# the optional cookie password.
|
324
349
|
#
|
325
350
|
# @return WorkOS::RefreshAuthenticationResponse
|
326
351
|
def authenticate_with_refresh_token(
|
@@ -328,8 +353,13 @@ module WorkOS
|
|
328
353
|
client_id:,
|
329
354
|
organization_id: nil,
|
330
355
|
ip_address: nil,
|
331
|
-
user_agent: nil
|
356
|
+
user_agent: nil,
|
357
|
+
session: nil
|
332
358
|
)
|
359
|
+
if session && (session[:seal_session] == true) && session[:cookie_password].nil?
|
360
|
+
raise ArgumentError, 'cookie_password is required when sealing session'
|
361
|
+
end
|
362
|
+
|
333
363
|
response = execute_request(
|
334
364
|
request: post_request(
|
335
365
|
path: '/user_management/authenticate',
|
@@ -345,7 +375,7 @@ module WorkOS
|
|
345
375
|
),
|
346
376
|
)
|
347
377
|
|
348
|
-
WorkOS::RefreshAuthenticationResponse.new(response.body)
|
378
|
+
WorkOS::RefreshAuthenticationResponse.new(response.body, session)
|
349
379
|
end
|
350
380
|
|
351
381
|
# Authenticate user by Magic Auth Code.
|
data/lib/workos/version.rb
CHANGED
data/lib/workos.rb
CHANGED
@@ -71,6 +71,7 @@ module WorkOS
|
|
71
71
|
autoload :Profile, 'workos/profile'
|
72
72
|
autoload :ProfileAndToken, 'workos/profile_and_token'
|
73
73
|
autoload :RefreshAuthenticationResponse, 'workos/refresh_authentication_response'
|
74
|
+
autoload :Session, 'workos/session'
|
74
75
|
autoload :SSO, 'workos/sso'
|
75
76
|
autoload :Types, 'workos/types'
|
76
77
|
autoload :User, 'workos/user'
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe WorkOS::Session do
|
4
|
+
let(:user_management) { instance_double('UserManagement') }
|
5
|
+
let(:client_id) { 'test_client_id' }
|
6
|
+
let(:cookie_password) { 'test_very_long_cookie_password__' }
|
7
|
+
let(:session_data) { 'test_session_data' }
|
8
|
+
let(:jwks_url) { 'https://api.workos.com/sso/jwks/client_123' }
|
9
|
+
let(:jwks_hash) { '{"keys":[{"alg":"RS256","kty":"RSA","use":"sig","n":"test_n","e":"AQAB","kid":"sso_oidc_key_pair_123","x5c":["test"],"x5t#S256":"test"}]}' } # rubocop:disable all
|
10
|
+
let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), { kid: 'sso_oidc_key_pair_123', use: 'sig', alg: 'RS256' }) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
|
14
|
+
allow(Net::HTTP).to receive(:get).and_return(jwks_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'initialize' do
|
18
|
+
it 'raises an error if cookie_password is nil or empty' do
|
19
|
+
expect do
|
20
|
+
WorkOS::Session.new(
|
21
|
+
user_management: user_management,
|
22
|
+
client_id: client_id,
|
23
|
+
session_data: session_data,
|
24
|
+
cookie_password: nil,
|
25
|
+
)
|
26
|
+
end.to raise_error(ArgumentError, 'cookiePassword is required')
|
27
|
+
|
28
|
+
expect do
|
29
|
+
WorkOS::Session.new(
|
30
|
+
user_management: user_management,
|
31
|
+
client_id: client_id,
|
32
|
+
session_data: session_data,
|
33
|
+
cookie_password: '',
|
34
|
+
)
|
35
|
+
end.to raise_error(ArgumentError, 'cookiePassword is required')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'initializes with valid parameters' do
|
39
|
+
session = WorkOS::Session.new(
|
40
|
+
user_management: user_management,
|
41
|
+
client_id: client_id,
|
42
|
+
session_data: session_data,
|
43
|
+
cookie_password: cookie_password,
|
44
|
+
)
|
45
|
+
expect(session.user_management).to eq(user_management)
|
46
|
+
expect(session.client_id).to eq(client_id)
|
47
|
+
expect(session.session_data).to eq(session_data)
|
48
|
+
expect(session.cookie_password).to eq(cookie_password)
|
49
|
+
expect(session.jwks.map(&:export)).to eq(JSON.parse(jwks_hash, symbolize_names: true)[:keys])
|
50
|
+
expect(session.jwks_algorithms).to eq(['RS256'])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '.authenticate' do
|
55
|
+
let(:valid_access_token) do
|
56
|
+
payload = {
|
57
|
+
sid: 'session_id',
|
58
|
+
org_id: 'org_id',
|
59
|
+
role: 'role',
|
60
|
+
permissions: ['read'],
|
61
|
+
exp: Time.now.to_i + 3600,
|
62
|
+
}
|
63
|
+
headers = { kid: jwk[:kid] }
|
64
|
+
JWT.encode(payload, jwk.signing_key, jwk[:alg], headers)
|
65
|
+
end
|
66
|
+
let(:session_data) do
|
67
|
+
WorkOS::Session.seal_data({
|
68
|
+
access_token: valid_access_token,
|
69
|
+
user: 'user',
|
70
|
+
impersonator: 'impersonator',
|
71
|
+
}, cookie_password,)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns NO_SESSION_COOKIE_PROVIDED if session_data is nil' do
|
75
|
+
session = WorkOS::Session.new(
|
76
|
+
user_management: user_management,
|
77
|
+
client_id: client_id,
|
78
|
+
session_data: nil,
|
79
|
+
cookie_password: cookie_password,
|
80
|
+
)
|
81
|
+
result = session.authenticate
|
82
|
+
expect(result).to eq({ authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' })
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'returns INVALID_SESSION_COOKIE if session_data is invalid' do
|
86
|
+
session = WorkOS::Session.new(
|
87
|
+
user_management: user_management,
|
88
|
+
client_id: client_id,
|
89
|
+
session_data: 'invalid_data',
|
90
|
+
cookie_password: cookie_password,
|
91
|
+
)
|
92
|
+
result = session.authenticate
|
93
|
+
expect(result).to eq({ authenticated: false, reason: 'INVALID_SESSION_COOKIE' })
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'returns INVALID_JWT if access_token is invalid' do
|
97
|
+
invalid_session_data = WorkOS::Session.seal_data({ access_token: 'invalid_token' }, cookie_password)
|
98
|
+
session = WorkOS::Session.new(
|
99
|
+
user_management: user_management,
|
100
|
+
client_id: client_id,
|
101
|
+
session_data: invalid_session_data,
|
102
|
+
cookie_password: cookie_password,
|
103
|
+
)
|
104
|
+
result = session.authenticate
|
105
|
+
expect(result).to eq({ authenticated: false, reason: 'INVALID_JWT' })
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'authenticates successfully with valid session_data' do
|
109
|
+
session = WorkOS::Session.new(
|
110
|
+
user_management: user_management,
|
111
|
+
client_id: client_id,
|
112
|
+
session_data: session_data,
|
113
|
+
cookie_password: cookie_password,
|
114
|
+
)
|
115
|
+
allow(session).to receive(:is_valid_jwt).and_return(true)
|
116
|
+
allow(JWT).to receive(:decode).and_return([{
|
117
|
+
'sid' => 'session_id',
|
118
|
+
'org_id' => 'org_id',
|
119
|
+
'role' => 'role',
|
120
|
+
'permissions' => ['read'],
|
121
|
+
}])
|
122
|
+
|
123
|
+
result = session.authenticate
|
124
|
+
expect(result).to eq({
|
125
|
+
authenticated: true,
|
126
|
+
session_id: 'session_id',
|
127
|
+
organization_id: 'org_id',
|
128
|
+
role: 'role',
|
129
|
+
permissions: ['read'],
|
130
|
+
user: 'user',
|
131
|
+
impersonator: 'impersonator',
|
132
|
+
reason: nil,
|
133
|
+
})
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '.refresh' do
|
138
|
+
let(:refresh_token) { 'test_refresh_token' }
|
139
|
+
let(:session_data) { WorkOS::Session.seal_data({ refresh_token: refresh_token, user: 'user' }, cookie_password) }
|
140
|
+
let(:auth_response) { double('AuthResponse', sealed_session: 'new_sealed_session') }
|
141
|
+
|
142
|
+
before do
|
143
|
+
allow(user_management).to receive(:authenticate_with_refresh_token).and_return(auth_response)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'returns INVALID_SESSION_COOKIE if session_data is invalid' do
|
147
|
+
session = WorkOS::Session.new(
|
148
|
+
user_management: user_management,
|
149
|
+
client_id: client_id,
|
150
|
+
session_data: 'invalid_data',
|
151
|
+
cookie_password: cookie_password,
|
152
|
+
)
|
153
|
+
result = session.refresh
|
154
|
+
expect(result).to eq({ authenticated: false, reason: 'INVALID_SESSION_COOKIE' })
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'refreshes the session successfully with valid session_data' do
|
158
|
+
session = WorkOS::Session.new(
|
159
|
+
user_management: user_management,
|
160
|
+
client_id: client_id,
|
161
|
+
session_data: session_data,
|
162
|
+
cookie_password: cookie_password,
|
163
|
+
)
|
164
|
+
result = session.refresh
|
165
|
+
expect(result).to eq({
|
166
|
+
authenticated: true,
|
167
|
+
sealed_session: 'new_sealed_session',
|
168
|
+
session: auth_response,
|
169
|
+
reason: nil,
|
170
|
+
})
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '.get_logout_url' do
|
175
|
+
let(:session) do
|
176
|
+
WorkOS::Session.new(
|
177
|
+
user_management: user_management,
|
178
|
+
client_id: client_id,
|
179
|
+
session_data: session_data,
|
180
|
+
cookie_password: cookie_password,
|
181
|
+
)
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'when authentication is successful' do
|
185
|
+
before do
|
186
|
+
allow(session).to receive(:authenticate).and_return({
|
187
|
+
authenticated: true,
|
188
|
+
session_id: 'session_id',
|
189
|
+
reason: nil,
|
190
|
+
})
|
191
|
+
allow(user_management).to receive(:get_logout_url).with(session_id: 'session_id').and_return('https://example.com/logout')
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'returns the logout URL' do
|
195
|
+
expect(session.get_logout_url).to eq('https://example.com/logout')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
context 'when authentication fails' do
|
200
|
+
before do
|
201
|
+
allow(session).to receive(:authenticate).and_return({
|
202
|
+
authenticated: false,
|
203
|
+
reason: 'Invalid session',
|
204
|
+
})
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'raises an error' do
|
208
|
+
expect { session.get_logout_url }.to raise_error(
|
209
|
+
RuntimeError, 'Failed to extract session ID for logout URL: Invalid session',
|
210
|
+
)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/spec/lib/workos/sso_spec.rb
CHANGED
@@ -302,6 +302,9 @@ describe WorkOS::SSO do
|
|
302
302
|
id: 'prof_01EEJTY9SZ1R350RB7B73SNBKF',
|
303
303
|
idp_id: '116485463307139932699',
|
304
304
|
last_name: 'Loblaw',
|
305
|
+
role: {
|
306
|
+
slug: 'member',
|
307
|
+
},
|
305
308
|
groups: nil,
|
306
309
|
organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
|
307
310
|
raw_attributes: {
|
@@ -373,6 +376,9 @@ describe WorkOS::SSO do
|
|
373
376
|
id: 'prof_01DRA1XNSJDZ19A31F183ECQW5',
|
374
377
|
idp_id: '00u1klkowm8EGah2H357',
|
375
378
|
last_name: 'Demo',
|
379
|
+
role: {
|
380
|
+
slug: 'admin',
|
381
|
+
},
|
376
382
|
groups: %w[Admins Developers],
|
377
383
|
organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
|
378
384
|
raw_attributes: {
|
@@ -467,6 +467,7 @@ describe WorkOS::UserManagement do
|
|
467
467
|
)
|
468
468
|
expect(authentication_response.access_token).to eq('<ACCESS_TOKEN>')
|
469
469
|
expect(authentication_response.refresh_token).to eq('<REFRESH_TOKEN>')
|
470
|
+
expect(authentication_response.user.id).to eq('user_01H93WD0R0KWF8Q7BK02C0RPYJ')
|
470
471
|
end
|
471
472
|
end
|
472
473
|
end
|
@@ -67,7 +67,7 @@ http_interactions:
|
|
67
67
|
body:
|
68
68
|
encoding: UTF-8
|
69
69
|
string:
|
70
|
-
'{"object":"profile","id":"prof_01EEJTY9SZ1R350RB7B73SNBKF","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01E83FVYZHY7DM4S9503JHV0R5","connection_type":"GoogleOAuth","idp_id":"116485463307139932699","email":"bob.loblaw@workos.com","first_name":"Bob","last_name":"Loblaw","raw_attributes":{"hd":"workos.com","id":"116485463307139932699","name":"Bob
|
70
|
+
'{"object":"profile","id":"prof_01EEJTY9SZ1R350RB7B73SNBKF","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01E83FVYZHY7DM4S9503JHV0R5","connection_type":"GoogleOAuth","idp_id":"116485463307139932699","email":"bob.loblaw@workos.com","first_name":"Bob","last_name":"Loblaw","role":{"slug":"member"},"raw_attributes":{"hd":"workos.com","id":"116485463307139932699","name":"Bob
|
71
71
|
Loblaw","email":"bob.loblaw@workos.com","locale":"en","picture":"https://lh3.googleusercontent.com/a-/AOh14GyO2hLlgZvteDQ3Ldi3_-RteZLya0hWH7247Cam=s96-c","given_name":"Bob","family_name":"Loblaw","verified_email":true}}'
|
72
72
|
http_version:
|
73
73
|
recorded_at: Tue, 18 May 2021 22:55:21 GMT
|
data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml
CHANGED
@@ -1,81 +1,82 @@
|
|
1
1
|
---
|
2
2
|
http_interactions:
|
3
|
-
- request:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: https://api.workos.com/user_management/authenticate
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string:
|
9
|
+
'{"refresh_token":"some_refresh_token","client_id":"client_123","client_secret":"<API_KEY>","ip_address":"200.240.210.16","user_agent":"Mozilla/5.0
|
10
|
+
(Macintosh; Intel Mac OS X 10_15_7) Chrome/108.0.0.0 Safari/537.36","grant_type":"refresh_token"}'
|
11
|
+
headers:
|
12
|
+
Content-Type:
|
13
|
+
- application/json
|
14
|
+
Accept-Encoding:
|
15
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
16
|
+
Accept:
|
17
|
+
- '*/*'
|
18
|
+
User-Agent:
|
19
|
+
- WorkOS; ruby/3.2.2; arm64-darwin22; v4.0.0
|
20
|
+
response:
|
21
|
+
status:
|
22
|
+
code: 200
|
23
|
+
message: OK
|
24
|
+
headers:
|
25
|
+
Date:
|
26
|
+
- Mon, 18 Mar 2024 19:00:53 GMT
|
27
|
+
Content-Type:
|
28
|
+
- application/json; charset=utf-8
|
29
|
+
Transfer-Encoding:
|
30
|
+
- chunked
|
31
|
+
Connection:
|
32
|
+
- keep-alive
|
33
|
+
Cf-Ray:
|
34
|
+
- 866777d63b4627e8-SLC
|
35
|
+
Cf-Cache-Status:
|
36
|
+
- DYNAMIC
|
37
|
+
Etag:
|
38
|
+
- W/"335-M3MDQYhs5724SayBHHCwnBDn3qA"
|
39
|
+
Strict-Transport-Security:
|
40
|
+
- max-age=15552000; includeSubDomains
|
41
|
+
Vary:
|
42
|
+
- Origin, Accept-Encoding
|
43
|
+
Via:
|
44
|
+
- 1.1 spaces-router (devel)
|
45
|
+
Access-Control-Allow-Credentials:
|
46
|
+
- 'true'
|
47
|
+
Content-Security-Policy:
|
48
|
+
- "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self'
|
49
|
+
https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src
|
50
|
+
'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
|
51
|
+
Expect-Ct:
|
52
|
+
- max-age=0
|
53
|
+
Referrer-Policy:
|
54
|
+
- no-referrer
|
55
|
+
X-Content-Type-Options:
|
56
|
+
- nosniff
|
57
|
+
X-Dns-Prefetch-Control:
|
58
|
+
- 'off'
|
59
|
+
X-Download-Options:
|
60
|
+
- noopen
|
61
|
+
X-Frame-Options:
|
62
|
+
- SAMEORIGIN
|
63
|
+
X-Permitted-Cross-Domain-Policies:
|
64
|
+
- none
|
65
|
+
X-Request-Id:
|
66
|
+
- 995ed1ed-e892-4049-86c9-0e07baa6cc4b
|
67
|
+
X-Xss-Protection:
|
68
|
+
- '0'
|
69
|
+
Set-Cookie:
|
70
|
+
- __cf_bm=2NHqv1cd1BisOc8KKcQ0oNzFxZZT4OHQd6c2QDuGnUU-1710788453-1.0.1.1-4BxBRzVrhL7rCH895PcfORXr_6Rnj3Oh5w1YG4xi7X1st62LMzb5dHZO7u7P.V1P8nBDAAt3Wbz7xsDTWrfWJg;
|
71
|
+
path=/; expires=Mon, 18-Mar-24 19:30:53 GMT; domain=.workos.com; HttpOnly;
|
72
|
+
Secure; SameSite=None
|
73
|
+
- __cfruid=06035c17e9b60a1d7a42a5b568146a0bb71a06dc-1710788453; path=/; domain=.workos.com;
|
74
|
+
HttpOnly; Secure; SameSite=None
|
75
|
+
Server:
|
76
|
+
- cloudflare
|
77
|
+
body:
|
78
|
+
encoding: UTF-8
|
79
|
+
string: '{"user":{"object":"user","id":"user_01H93WD0R0KWF8Q7BK02C0RPYJ","email":"test@workos.app","email_verified":true,"first_name":"Lucille","last_name":"Bluth","created_at":"2023-08-30T18:48:26.517Z","updated_at":"2023-08-30T18:58:00.821Z","user_type":"unmanaged","email_verified_at":"2023-08-30T18:58:00.915Z","google_oauth_profile_id":null,"microsoft_oauth_profile_id":null},"access_token":"<ACCESS_TOKEN>","refresh_token":"<REFRESH_TOKEN>"}'
|
80
|
+
http_version:
|
81
|
+
recorded_at: Mon, 18 Mar 2024 19:00:53 GMT
|
81
82
|
recorded_with: VCR 5.0.0
|
data/spec/support/profile.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"profile":{"object":"profile","id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01EMH8WAK20T42N2NBMNBCYHAG","connection_type":"OktaSAML","last_name":"Demo","groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357","raw_attributes":{"id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","last_name":"Demo","groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357"}},"access_token":"01DVX6QBS3EG6FHY2ESAA5Q65X"}
|
1
|
+
{"profile":{"object":"profile","id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01EMH8WAK20T42N2NBMNBCYHAG","connection_type":"OktaSAML","last_name":"Demo","role":{"slug": "admin"},"groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357","raw_attributes":{"id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","last_name":"Demo","groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357"}},"access_token":"01DVX6QBS3EG6FHY2ESAA5Q65X"}
|
data/workos.gemspec
CHANGED
@@ -21,6 +21,9 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
22
|
spec.require_paths = ['lib']
|
23
23
|
|
24
|
+
spec.add_dependency 'encryptor', '~> 3.0'
|
25
|
+
spec.add_dependency 'jwt', '~> 2.8'
|
26
|
+
|
24
27
|
spec.add_development_dependency 'bundler', '>= 2.0.1'
|
25
28
|
spec.add_development_dependency 'rspec', '~> 3.9.0'
|
26
29
|
spec.add_development_dependency 'rubocop', '~> 0.77'
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: workos
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WorkOS
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: encryptor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jwt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.8'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: bundler
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -137,6 +165,7 @@ files:
|
|
137
165
|
- lib/workos/profile.rb
|
138
166
|
- lib/workos/profile_and_token.rb
|
139
167
|
- lib/workos/refresh_authentication_response.rb
|
168
|
+
- lib/workos/session.rb
|
140
169
|
- lib/workos/sso.rb
|
141
170
|
- lib/workos/types.rb
|
142
171
|
- lib/workos/types/intent.rb
|
@@ -161,6 +190,7 @@ files:
|
|
161
190
|
- spec/lib/workos/organizations_spec.rb
|
162
191
|
- spec/lib/workos/passwordless_spec.rb
|
163
192
|
- spec/lib/workos/portal_spec.rb
|
193
|
+
- spec/lib/workos/session_spec.rb
|
164
194
|
- spec/lib/workos/sso_spec.rb
|
165
195
|
- spec/lib/workos/user_management_spec.rb
|
166
196
|
- spec/lib/workos/webhooks_spec.rb
|
@@ -373,6 +403,7 @@ test_files:
|
|
373
403
|
- spec/lib/workos/organizations_spec.rb
|
374
404
|
- spec/lib/workos/passwordless_spec.rb
|
375
405
|
- spec/lib/workos/portal_spec.rb
|
406
|
+
- spec/lib/workos/session_spec.rb
|
376
407
|
- spec/lib/workos/sso_spec.rb
|
377
408
|
- spec/lib/workos/user_management_spec.rb
|
378
409
|
- spec/lib/workos/webhooks_spec.rb
|