workos 5.5.1 → 5.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +7 -1
- data/lib/workos/authentication_response.rb +25 -4
- data/lib/workos/refresh_authentication_response.rb +25 -2
- data/lib/workos/session.rb +183 -0
- data/lib/workos/types/intent.rb +2 -1
- 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/portal_spec.rb +15 -0
- data/spec/lib/workos/session_spec.rb +214 -0
- data/spec/lib/workos/user_management_spec.rb +1 -0
- data/spec/support/fixtures/vcr_cassettes/portal/generate_link_certificate_renewal.yml +72 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml +79 -78
- data/workos.gemspec +3 -0
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c1790feb170b50e199cef2297443aef2021978b3e1fe0c48c28e9b144e4120b
|
4
|
+
data.tar.gz: 1b78e33b06c689280525ffb2f94847a86d17f2c9001ba229a16745511698442f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4f641c69ad3f57def5fbb4533aef65a7fd294b59e169e4ef1d141cea58732643417727b1c84d510a3cf379e28f9b4fad931aea6d2c9cf50cf84765be29033d5
|
7
|
+
data.tar.gz: 58f22a8a48d35941994f9877adbc02ed6ec2dd7cf9b6107401156cc8665912a54956e304aa64229ebd5515ddab14977ae293768700171ee7a1593efff272f59d
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
workos (5.
|
4
|
+
workos (5.7.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
|
@@ -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
|
data/lib/workos/types/intent.rb
CHANGED
@@ -6,11 +6,12 @@ module WorkOS
|
|
6
6
|
# intents while generating an Admin Portal link.
|
7
7
|
module Intent
|
8
8
|
AUDIT_LOGS = 'audit_logs'
|
9
|
+
CERTIFICATE_RENEWAL = 'certificate_renewal'
|
9
10
|
DSYNC = 'dsync'
|
10
11
|
LOG_STREAMS = 'log_streams'
|
11
12
|
SSO = 'sso'
|
12
13
|
|
13
|
-
ALL = [AUDIT_LOGS, DSYNC, LOG_STREAMS, SSO].freeze
|
14
|
+
ALL = [AUDIT_LOGS, CERTIFICATE_RENEWAL, DSYNC, LOG_STREAMS, SSO].freeze
|
14
15
|
end
|
15
16
|
end
|
16
17
|
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'
|
@@ -51,6 +51,21 @@ describe WorkOS::Portal do
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
|
+
|
55
|
+
describe 'with the certificate_renewal intent' do
|
56
|
+
it 'returns an Admin Portal link' do
|
57
|
+
VCR.use_cassette 'portal/generate_link_certificate_renewal', match_requests_on: %i[path body] do
|
58
|
+
portal_link = described_class.generate_link(
|
59
|
+
intent: 'certificate_renewal',
|
60
|
+
organization: organization,
|
61
|
+
)
|
62
|
+
|
63
|
+
expect(portal_link).to eq(
|
64
|
+
'https://id.workos.com/portal/launch?secret=secret',
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
54
69
|
end
|
55
70
|
|
56
71
|
describe 'with an invalid organization' do
|
@@ -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
|
@@ -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
|
@@ -0,0 +1,72 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: https://api.workos.com/portal/generate_link
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: '{"intent":"certificate_renewal","organization":"org_01EHQMYV6MBK39QC5PZXHY59C3","return_url":null,"success_url":null}'
|
9
|
+
headers:
|
10
|
+
Content-Type:
|
11
|
+
- application/json
|
12
|
+
Accept-Encoding:
|
13
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
14
|
+
Accept:
|
15
|
+
- "*/*"
|
16
|
+
User-Agent:
|
17
|
+
- WorkOS; ruby/2.7.1; x86_64-darwin19; v0.5.0
|
18
|
+
Authorization:
|
19
|
+
- Bearer <API_KEY>
|
20
|
+
response:
|
21
|
+
status:
|
22
|
+
code: 201
|
23
|
+
message: Created
|
24
|
+
headers:
|
25
|
+
Server:
|
26
|
+
- Cowboy
|
27
|
+
Connection:
|
28
|
+
- keep-alive
|
29
|
+
Vary:
|
30
|
+
- Origin, Accept-Encoding
|
31
|
+
Access-Control-Allow-Credentials:
|
32
|
+
- 'true'
|
33
|
+
Content-Security-Policy:
|
34
|
+
- 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
|
35
|
+
https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
|
36
|
+
''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
|
37
|
+
X-Dns-Prefetch-Control:
|
38
|
+
- 'off'
|
39
|
+
Expect-Ct:
|
40
|
+
- max-age=0
|
41
|
+
X-Frame-Options:
|
42
|
+
- SAMEORIGIN
|
43
|
+
Strict-Transport-Security:
|
44
|
+
- max-age=15552000; includeSubDomains
|
45
|
+
X-Download-Options:
|
46
|
+
- noopen
|
47
|
+
X-Content-Type-Options:
|
48
|
+
- nosniff
|
49
|
+
X-Permitted-Cross-Domain-Policies:
|
50
|
+
- none
|
51
|
+
Referrer-Policy:
|
52
|
+
- no-referrer
|
53
|
+
X-Xss-Protection:
|
54
|
+
- '0'
|
55
|
+
X-Request-Id:
|
56
|
+
- cb9ad5cf-243a-4084-a4f6-2d7d2b097b8b
|
57
|
+
Content-Type:
|
58
|
+
- application/json; charset=utf-8
|
59
|
+
Content-Length:
|
60
|
+
- '79'
|
61
|
+
Etag:
|
62
|
+
- W/"4f-NN86NUZRu/GQgPAYTexTS6/9DnM"
|
63
|
+
Date:
|
64
|
+
- Wed, 09 Sep 2020 23:43:07 GMT
|
65
|
+
Via:
|
66
|
+
- 1.1 vegur
|
67
|
+
body:
|
68
|
+
encoding: UTF-8
|
69
|
+
string: '{"link":"https://id.workos.com/portal/launch?secret=secret"}'
|
70
|
+
http_version:
|
71
|
+
recorded_at: Wed, 09 Sep 2020 23:43:07 GMT
|
72
|
+
recorded_with: VCR 5.0.0
|
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/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.7.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-08-
|
11
|
+
date: 2024-08-29 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
|
@@ -241,6 +271,7 @@ files:
|
|
241
271
|
- spec/support/fixtures/vcr_cassettes/passwordless/send_session.yml
|
242
272
|
- spec/support/fixtures/vcr_cassettes/passwordless/send_session_invalid.yml
|
243
273
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_audit_logs.yml
|
274
|
+
- spec/support/fixtures/vcr_cassettes/portal/generate_link_certificate_renewal.yml
|
244
275
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_dsync.yml
|
245
276
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_invalid.yml
|
246
277
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_sso.yml
|
@@ -372,6 +403,7 @@ test_files:
|
|
372
403
|
- spec/lib/workos/organizations_spec.rb
|
373
404
|
- spec/lib/workos/passwordless_spec.rb
|
374
405
|
- spec/lib/workos/portal_spec.rb
|
406
|
+
- spec/lib/workos/session_spec.rb
|
375
407
|
- spec/lib/workos/sso_spec.rb
|
376
408
|
- spec/lib/workos/user_management_spec.rb
|
377
409
|
- spec/lib/workos/webhooks_spec.rb
|
@@ -452,6 +484,7 @@ test_files:
|
|
452
484
|
- spec/support/fixtures/vcr_cassettes/passwordless/send_session.yml
|
453
485
|
- spec/support/fixtures/vcr_cassettes/passwordless/send_session_invalid.yml
|
454
486
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_audit_logs.yml
|
487
|
+
- spec/support/fixtures/vcr_cassettes/portal/generate_link_certificate_renewal.yml
|
455
488
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_dsync.yml
|
456
489
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_invalid.yml
|
457
490
|
- spec/support/fixtures/vcr_cassettes/portal/generate_link_sso.yml
|