workos 5.10.0 → 5.11.1

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: 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