workos 5.6.0 → 5.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aee47792913b272d66e3adb2d504793a7ae40b756c9f5ee545bf407df78ff4c4
4
- data.tar.gz: 33e3cc29078c68806f2cbf463945c11153c2e92d75bc8def9898a14c15623118
3
+ metadata.gz: 9c1790feb170b50e199cef2297443aef2021978b3e1fe0c48c28e9b144e4120b
4
+ data.tar.gz: 1b78e33b06c689280525ffb2f94847a86d17f2c9001ba229a16745511698442f
5
5
  SHA512:
6
- metadata.gz: ba6b7b8ad5f8715ad599cf4228a52970d4dd32834b5f35b35fc534ffc066e0b12e431e3ff16d9f30dbe4c5c88f49651e71d891f921bd8fd534a3276a1bf2a5dc
7
- data.tar.gz: 388f1a1bb1235012b25c336052b51e4a142c2d225af938cb7aa12e01580003ae7430e7a4e2c4b55f81508edc0547a4d6e7df05b2d9c31e636b5a49736cd48233
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.6.0)
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, :organization_id, :impersonator, :access_token, :refresh_token
9
+ attr_accessor :user,
10
+ :organization_id,
11
+ :impersonator,
12
+ :access_token,
13
+ :refresh_token,
14
+ :authentication_method,
15
+ :sealed_session
10
16
 
11
- def initialize(authentication_response_json)
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
- @access_token = json[:access_token]
21
- @refresh_token = json[:refresh_token]
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
- def initialize(authentication_response_json)
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WorkOS
4
- VERSION = '5.6.0'
4
+ VERSION = '5.7.0'
5
5
  end
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
@@ -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
@@ -1,81 +1,82 @@
1
1
  ---
2
2
  http_interactions:
3
- - request:
4
- method: post
5
- uri: https://api.workos.com/user_management/authenticate
6
- body:
7
- encoding: UTF-8
8
- string: '{"refresh_token":"some_refresh_token","client_id":"client_123","client_secret":"<API_KEY>","ip_address":"200.240.210.16","user_agent":"Mozilla/5.0
9
- (Macintosh; Intel Mac OS X 10_15_7) Chrome/108.0.0.0 Safari/537.36","grant_type":"refresh_token"}'
10
- headers:
11
- Content-Type:
12
- - application/json
13
- Accept-Encoding:
14
- - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
15
- Accept:
16
- - "*/*"
17
- User-Agent:
18
- - WorkOS; ruby/3.2.2; arm64-darwin22; v4.0.0
19
- response:
20
- status:
21
- code: 200
22
- message: OK
23
- headers:
24
- Date:
25
- - Mon, 18 Mar 2024 19:00:53 GMT
26
- Content-Type:
27
- - application/json; charset=utf-8
28
- Transfer-Encoding:
29
- - chunked
30
- Connection:
31
- - keep-alive
32
- Cf-Ray:
33
- - 866777d63b4627e8-SLC
34
- Cf-Cache-Status:
35
- - DYNAMIC
36
- Etag:
37
- - W/"335-M3MDQYhs5724SayBHHCwnBDn3qA"
38
- Strict-Transport-Security:
39
- - max-age=15552000; includeSubDomains
40
- Vary:
41
- - Origin, Accept-Encoding
42
- Via:
43
- - 1.1 spaces-router (devel)
44
- Access-Control-Allow-Credentials:
45
- - 'true'
46
- Content-Security-Policy:
47
- - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
48
- https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
49
- ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
50
- Expect-Ct:
51
- - max-age=0
52
- Referrer-Policy:
53
- - no-referrer
54
- X-Content-Type-Options:
55
- - nosniff
56
- X-Dns-Prefetch-Control:
57
- - 'off'
58
- X-Download-Options:
59
- - noopen
60
- X-Frame-Options:
61
- - SAMEORIGIN
62
- X-Permitted-Cross-Domain-Policies:
63
- - none
64
- X-Request-Id:
65
- - 995ed1ed-e892-4049-86c9-0e07baa6cc4b
66
- X-Xss-Protection:
67
- - '0'
68
- Set-Cookie:
69
- - __cf_bm=2NHqv1cd1BisOc8KKcQ0oNzFxZZT4OHQd6c2QDuGnUU-1710788453-1.0.1.1-4BxBRzVrhL7rCH895PcfORXr_6Rnj3Oh5w1YG4xi7X1st62LMzb5dHZO7u7P.V1P8nBDAAt3Wbz7xsDTWrfWJg;
70
- path=/; expires=Mon, 18-Mar-24 19:30:53 GMT; domain=.workos.com; HttpOnly;
71
- Secure; SameSite=None
72
- - __cfruid=06035c17e9b60a1d7a42a5b568146a0bb71a06dc-1710788453; path=/; domain=.workos.com;
73
- HttpOnly; Secure; SameSite=None
74
- Server:
75
- - cloudflare
76
- body:
77
- encoding: UTF-8
78
- string: '{"access_token":"<ACCESS_TOKEN>","refresh_token":"<REFRESH_TOKEN>"}'
79
- http_version:
80
- recorded_at: Mon, 18 Mar 2024 19:00:53 GMT
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.6.0
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-19 00:00:00.000000000 Z
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
@@ -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