workos 5.10.0 → 5.11.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83c350d6c017c0cf423adb02925391b3a5e11d622479d76073c3c6372e526105
4
- data.tar.gz: 83a8e5700dc7a3d47d37a84de01f866997a260eeaf96c55fd40318a30195a7d7
3
+ metadata.gz: 2792dbbd0e3dac4d2a9eac0920a135b0db63c723a85fbbec049327ab2cffd547
4
+ data.tar.gz: 356cb856f6e2df599daf2affc94eb74d4ee885ab71d77e6002ab176718dbc0ee
5
5
  SHA512:
6
- metadata.gz: 48bcc853e186de15ce9e71e98415d801e412540a43fe1711ab97264b3419ce7dc7c1ec6095411cc7093bcb788b02ab51efe4689c0a79993f921e037ce0a7954c
7
- data.tar.gz: f52aec8320aa98bb11ec114ffccb51a82218c7581cac202f74facfe943e96633b90262a67c37ed0677c73f4f73bb3bdd77760a67471c80a18ca878e0d7dffb55
6
+ metadata.gz: d4a1406d88ac981d0d0653027cf2afe17b64d7c577d6cfe0d4ccc9ffb0a6c6be29c0f3ed140a03bb8ce5a8876990e41bea60a892f5675dc6578ce1526a4504f6
7
+ data.tar.gz: 55abb8c32270ce7b4b5e4e7142f9f6c390e9cca88c3f3eb1242840193f4155da9bf4f018d85e5b34f51ebd3cc85f0cac176c8cd00abffd077133962100083566
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workos (5.10.0)
4
+ workos (5.11.1)
5
5
  encryptor (~> 3.0)
6
6
  jwt (~> 2.8)
7
7
 
@@ -19,7 +19,7 @@ GEM
19
19
  diff-lcs (1.5.1)
20
20
  encryptor (3.0.0)
21
21
  hashdiff (1.1.0)
22
- jwt (2.8.2)
22
+ jwt (2.10.1)
23
23
  base64
24
24
  parallel (1.24.0)
25
25
  parser (3.3.0.5)
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WorkOS
4
+ # The Cache module provides a simple in-memory cache for storing values
5
+ # This module is not meant to be instantiated in a user space, and is used internally by the SDK
6
+ module Cache
7
+ # The Entry class represents a cache entry with a value and an expiration time
8
+ class Entry
9
+ attr_reader :value, :expires_at
10
+
11
+ # Initializes a new cache entry
12
+ # @param value [Object] The value to store in the cache
13
+ # @param expires_in_seconds [Integer, nil] The expiration time for the value in seconds, or nil for no expiration
14
+ def initialize(value, expires_in_seconds)
15
+ @value = value
16
+ @expires_at = expires_in_seconds ? Time.now + expires_in_seconds : nil
17
+ end
18
+
19
+ # Checks if the entry has expired
20
+ # @return [Boolean] True if the entry has expired, false otherwise
21
+ def expired?
22
+ return false if expires_at.nil?
23
+
24
+ Time.now > @expires_at
25
+ end
26
+ end
27
+
28
+ class << self
29
+ # Fetches a value from the cache, or calls the block to fetch the value if it is not present
30
+ # @param key [String] The key to fetch the value for
31
+ # @param expires_in [Integer] The expiration time for the value in seconds
32
+ # @param force [Boolean] If true, the value will be fetched from the block even if it is present in the cache
33
+ # @param block [Proc] The block to call to fetch the value if it is not present in the cache
34
+ # @return [Object] The value fetched from the cache or the block
35
+ def fetch(key, expires_in: nil, force: false, &block)
36
+ entry = store[key]
37
+
38
+ if force || entry.nil? || entry.expired?
39
+ value = block.call
40
+ store[key] = Entry.new(value, expires_in)
41
+ return value
42
+ end
43
+
44
+ entry.value
45
+ end
46
+
47
+ # Reads a value from the cache
48
+ # @param key [String] The key to read the value for
49
+ # @return [Object] The value read from the cache, or nil if the value is not present or has expired
50
+ def read(key)
51
+ entry = store[key]
52
+ return nil if entry.nil? || entry.expired?
53
+
54
+ entry.value
55
+ end
56
+
57
+ # Writes a value to the cache
58
+ # @param key [String] The key to write the value for
59
+ # @param value [Object] The value to write to the cache
60
+ # @param expires_in [Integer] The expiration time for the value in seconds
61
+ # @return [Object] The value written to the cache
62
+ def write(key, value, expires_in: nil)
63
+ store[key] = Entry.new(value, expires_in)
64
+ value
65
+ end
66
+
67
+ # Deletes a value from the cache
68
+ # @param key [String] The key to delete the value for
69
+ def delete(key)
70
+ store.delete(key)
71
+ end
72
+
73
+ # Clears all values from the cache
74
+ def clear
75
+ store.clear
76
+ end
77
+
78
+ # Checks if a value exists in the cache
79
+ # @param key [String] The key to check for
80
+ # @return [Boolean] True if the value exists and has not expired, false otherwise
81
+ def exist?(key)
82
+ entry = store[key]
83
+ !(entry.nil? || entry.expired?)
84
+ end
85
+
86
+ private
87
+
88
+ # The in-memory store for the cache
89
+ def store
90
+ @store ||= {}
91
+ end
92
+ end
93
+ end
94
+ end
@@ -23,7 +23,9 @@ module WorkOS
23
23
  @session_data = session_data
24
24
  @client_id = client_id
25
25
 
26
- @jwks = create_remote_jwk_set(URI(@user_management.get_jwks_url(client_id)))
26
+ @jwks = Cache.fetch("jwks_#{client_id}", expires_in: 5 * 60) do
27
+ create_remote_jwk_set(URI(@user_management.get_jwks_url(client_id)))
28
+ end
27
29
  @jwks_algorithms = @jwks.map { |key| key[:alg] }.compact.uniq
28
30
  end
29
31
 
@@ -101,18 +103,17 @@ module WorkOS
101
103
  # rubocop:enable Metrics/PerceivedComplexity
102
104
 
103
105
  # Returns a URL to redirect the user to for logging out
106
+ # @param return_to [String] The URL to redirect the user to after logging out
104
107
  # @return [String] The URL to redirect the user to for logging out
105
- # rubocop:disable Naming/AccessorMethodName
106
- def get_logout_url
108
+ def get_logout_url(return_to: nil)
107
109
  auth_response = authenticate
108
110
 
109
111
  unless auth_response[:authenticated]
110
112
  raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}"
111
113
  end
112
114
 
113
- @user_management.get_logout_url(session_id: auth_response[:session_id])
115
+ @user_management.get_logout_url(session_id: auth_response[:session_id], return_to: return_to)
114
116
  end
115
- # rubocop:enable Naming/AccessorMethodName
116
117
 
117
118
  # Encrypts and seals data using AES-256-GCM
118
119
  # @param data [Hash] The data to seal
@@ -530,13 +530,17 @@ module WorkOS
530
530
  #
531
531
  # @param [String] session_id The session ID can be found in the `sid`
532
532
  # claim of the access token
533
+ # @param [String] return_to The URL to redirect the user to after logging out
533
534
  #
534
535
  # @return String
535
- def get_logout_url(session_id:)
536
+ def get_logout_url(session_id:, return_to: nil)
537
+ params = { session_id: session_id }
538
+ params[:return_to] = return_to if return_to
539
+
536
540
  URI::HTTPS.build(
537
541
  host: WorkOS.config.api_hostname,
538
542
  path: '/user_management/sessions/logout',
539
- query: "session_id=#{session_id}",
543
+ query: URI.encode_www_form(params),
540
544
  ).to_s
541
545
  end
542
546
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WorkOS
4
- VERSION = '5.10.0'
4
+ VERSION = '5.11.1'
5
5
  end
data/lib/workos.rb CHANGED
@@ -45,6 +45,7 @@ module WorkOS
45
45
  autoload :AuthenticationFactorAndChallenge, 'workos/authentication_factor_and_challenge'
46
46
  autoload :AuthenticationResponse, 'workos/authentication_response'
47
47
  autoload :AuditLogs, 'workos/audit_logs'
48
+ autoload :Cache, 'workos/cache'
48
49
  autoload :Challenge, 'workos/challenge'
49
50
  autoload :Client, 'workos/client'
50
51
  autoload :Connection, 'workos/connection'
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WorkOS::Cache do
4
+ before { described_class.clear }
5
+
6
+ describe '.write and .read' do
7
+ it 'stores and retrieves data' do
8
+ described_class.write('key', 'value')
9
+ expect(described_class.read('key')).to eq('value')
10
+ end
11
+
12
+ it 'returns nil if key does not exist' do
13
+ expect(described_class.read('missing')).to be_nil
14
+ end
15
+ end
16
+
17
+ describe '.fetch' do
18
+ it 'returns cached value when present and not expired' do
19
+ described_class.write('key', 'value')
20
+ fetch_value = described_class.fetch('key') { 'new_value' }
21
+ expect(fetch_value).to eq('value')
22
+ end
23
+
24
+ it 'executes block and caches value when not present' do
25
+ fetch_value = described_class.fetch('key') { 'new_value' }
26
+ expect(fetch_value).to eq('new_value')
27
+ end
28
+
29
+ it 'executes block and caches value when force is true' do
30
+ described_class.write('key', 'value')
31
+ fetch_value = described_class.fetch('key', force: true) { 'new_value' }
32
+ expect(fetch_value).to eq('new_value')
33
+ end
34
+ end
35
+
36
+ describe 'expiration' do
37
+ it 'expires values after specified time' do
38
+ described_class.write('key', 'value', expires_in: 0.1)
39
+ expect(described_class.read('key')).to eq('value')
40
+ sleep 0.2
41
+ expect(described_class.read('key')).to be_nil
42
+ end
43
+
44
+ it 'executes block and caches new value when expired' do
45
+ described_class.write('key', 'old_value', expires_in: 0.1)
46
+ sleep 0.2
47
+ fetch_value = described_class.fetch('key') { 'new_value' }
48
+ expect(fetch_value).to eq('new_value')
49
+ end
50
+
51
+ it 'does not expire values when expires_in is nil' do
52
+ described_class.write('key', 'value', expires_in: nil)
53
+ sleep 0.2
54
+ expect(described_class.read('key')).to eq('value')
55
+ end
56
+ end
57
+
58
+ describe '.exist?' do
59
+ it 'returns true if key exists' do
60
+ described_class.write('key', 'value')
61
+ expect(described_class.exist?('key')).to be true
62
+ end
63
+
64
+ it 'returns false if expired' do
65
+ described_class.write('key', 'value', expires_in: 0.1)
66
+ sleep 0.2
67
+ expect(described_class.exist?('key')).to be false
68
+ end
69
+
70
+ it 'returns false if key does not exist' do
71
+ expect(described_class.exist?('missing')).to be false
72
+ end
73
+ end
74
+
75
+ describe '.delete' do
76
+ it 'deletes key' do
77
+ described_class.write('key', 'value')
78
+ described_class.delete('key')
79
+ expect(described_class.read('key')).to be_nil
80
+ end
81
+ end
82
+
83
+ describe '.clear' do
84
+ it 'removes all keys from the cache' do
85
+ described_class.write('key1', 'value1')
86
+ described_class.write('key2', 'value2')
87
+
88
+ described_class.clear
89
+
90
+ expect(described_class.read('key1')).to be_nil
91
+ expect(described_class.read('key2')).to be_nil
92
+ end
93
+ end
94
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe WorkOS::Session do
4
- let(:user_management) { instance_double('UserManagement') }
5
4
  let(:client_id) { 'test_client_id' }
6
5
  let(:cookie_password) { 'test_very_long_cookie_password__' }
7
6
  let(:session_data) { 'test_session_data' }
@@ -10,11 +9,62 @@ describe WorkOS::Session do
10
9
  let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), { kid: 'sso_oidc_key_pair_123', use: 'sig', alg: 'RS256' }) }
11
10
 
12
11
  before do
13
- allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
14
12
  allow(Net::HTTP).to receive(:get).and_return(jwks_hash)
15
13
  end
16
14
 
17
15
  describe 'initialize' do
16
+ let(:user_management) { instance_double('UserManagement') }
17
+
18
+ before do
19
+ allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
20
+ end
21
+
22
+ describe 'JWKS caching' do
23
+ before do
24
+ WorkOS::Cache.clear
25
+ end
26
+
27
+ it 'caches and returns JWKS' do
28
+ expect(Net::HTTP).to receive(:get).once
29
+ session1 = WorkOS::Session.new(
30
+ user_management: user_management,
31
+ client_id: client_id,
32
+ session_data: session_data,
33
+ cookie_password: cookie_password,
34
+ )
35
+
36
+ session2 = WorkOS::Session.new(
37
+ user_management: user_management,
38
+ client_id: client_id,
39
+ session_data: session_data,
40
+ cookie_password: cookie_password,
41
+ )
42
+
43
+ expect(session1.jwks.map(&:export)).to eq(session2.jwks.map(&:export))
44
+ end
45
+
46
+ it 'fetches JWKS from remote when cache is expired' do
47
+ expect(Net::HTTP).to receive(:get).twice
48
+ session1 = WorkOS::Session.new(
49
+ user_management: user_management,
50
+ client_id: client_id,
51
+ session_data: session_data,
52
+ cookie_password: cookie_password,
53
+ )
54
+
55
+ allow(Time).to receive(:now).and_return(Time.now + 301)
56
+
57
+ session2 = WorkOS::Session.new(
58
+ user_management: user_management,
59
+ client_id: client_id,
60
+ session_data: session_data,
61
+ cookie_password: cookie_password,
62
+ )
63
+
64
+ expect(session1.jwks.map(&:export)).to eq(session2.jwks.map(&:export))
65
+ end
66
+ end
67
+
18
68
  it 'raises an error if cookie_password is nil or empty' do
19
69
  expect do
20
70
  WorkOS::Session.new(
@@ -52,6 +102,7 @@ describe WorkOS::Session do
52
102
  end
53
103
 
54
104
  describe '.authenticate' do
105
+ let(:user_management) { instance_double('UserManagement') }
55
106
  let(:valid_access_token) do
56
107
  payload = {
57
108
  sid: 'session_id',
@@ -71,6 +122,10 @@ describe WorkOS::Session do
71
122
  }, cookie_password,)
72
123
  end
73
124
 
125
+ before do
126
+ allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
127
+ end
128
+
74
129
  it 'returns NO_SESSION_COOKIE_PROVIDED if session_data is nil' do
75
130
  session = WorkOS::Session.new(
76
131
  user_management: user_management,
@@ -135,11 +190,13 @@ end
135
190
  end
136
191
 
137
192
  describe '.refresh' do
193
+ let(:user_management) { instance_double('UserManagement') }
138
194
  let(:refresh_token) { 'test_refresh_token' }
139
195
  let(:session_data) { WorkOS::Session.seal_data({ refresh_token: refresh_token, user: 'user' }, cookie_password) }
140
196
  let(:auth_response) { double('AuthResponse', sealed_session: 'new_sealed_session') }
141
197
 
142
198
  before do
199
+ allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
143
200
  allow(user_management).to receive(:authenticate_with_refresh_token).and_return(auth_response)
144
201
  end
145
202
 
@@ -173,26 +230,33 @@ end
173
230
 
174
231
  describe '.get_logout_url' do
175
232
  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
233
+ WorkOS::Session.new(
234
+ user_management: WorkOS::UserManagement,
235
+ client_id: client_id,
236
+ session_data: session_data,
237
+ cookie_password: cookie_password,
238
+ )
239
+ end
183
240
 
184
241
  context 'when authentication is successful' do
185
242
  before do
186
243
  allow(session).to receive(:authenticate).and_return({
187
244
  authenticated: true,
188
- session_id: 'session_id',
245
+ session_id: 'session_123abc',
189
246
  reason: nil,
190
247
  })
191
- allow(user_management).to receive(:get_logout_url).with(session_id: 'session_id').and_return('https://example.com/logout')
192
248
  end
193
249
 
194
250
  it 'returns the logout URL' do
195
- expect(session.get_logout_url).to eq('https://example.com/logout')
251
+ expect(session.get_logout_url).to eq('https://api.workos.com/user_management/sessions/logout?session_id=session_123abc')
252
+ end
253
+
254
+ context 'when given a return_to URL' do
255
+ it 'returns the logout URL with the return_to parameter' do
256
+ expect(session.get_logout_url(return_to: 'https://example.com/signed-out')).to eq(
257
+ 'https://api.workos.com/user_management/sessions/logout?session_id=session_123abc&return_to=https%3A%2F%2Fexample.com%2Fsigned-out',
258
+ )
259
+ end
196
260
  end
197
261
  end
198
262
 
@@ -1441,4 +1441,25 @@ describe WorkOS::UserManagement do
1441
1441
  end
1442
1442
  end
1443
1443
  end
1444
+
1445
+ describe '.get_logout_url' do
1446
+ it 'returns a logout url for the given session ID' do
1447
+ result = described_class.get_logout_url(
1448
+ session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6',
1449
+ )
1450
+
1451
+ expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6'
1452
+ end
1453
+
1454
+ context 'when a `return_to` is given' do
1455
+ it 'returns a logout url with the `return_to` query parameter' do
1456
+ result = described_class.get_logout_url(
1457
+ session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6',
1458
+ return_to: 'https://example.com/signed-out',
1459
+ )
1460
+
1461
+ expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6&return_to=https%3A%2F%2Fexample.com%2Fsigned-out'
1462
+ end
1463
+ end
1464
+ end
1444
1465
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workos
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.10.0
4
+ version: 5.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - WorkOS
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-06 00:00:00.000000000 Z
11
+ date: 2025-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: encryptor
@@ -136,6 +136,7 @@ files:
136
136
  - lib/workos/audit_logs.rb
137
137
  - lib/workos/authentication_factor_and_challenge.rb
138
138
  - lib/workos/authentication_response.rb
139
+ - lib/workos/cache.rb
139
140
  - lib/workos/challenge.rb
140
141
  - lib/workos/client.rb
141
142
  - lib/workos/configuration.rb
@@ -184,6 +185,7 @@ files:
184
185
  - lib/workos/webhooks.rb
185
186
  - lib/workos/widgets.rb
186
187
  - spec/lib/workos/audit_logs_spec.rb
188
+ - spec/lib/workos/cache_spec.rb
187
189
  - spec/lib/workos/client.rb
188
190
  - spec/lib/workos/configuration_spec.rb
189
191
  - spec/lib/workos/directory_sync_spec.rb
@@ -403,6 +405,7 @@ specification_version: 4
403
405
  summary: API client for WorkOS
404
406
  test_files:
405
407
  - spec/lib/workos/audit_logs_spec.rb
408
+ - spec/lib/workos/cache_spec.rb
406
409
  - spec/lib/workos/client.rb
407
410
  - spec/lib/workos/configuration_spec.rb
408
411
  - spec/lib/workos/directory_sync_spec.rb