workos 5.6.0 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aee47792913b272d66e3adb2d504793a7ae40b756c9f5ee545bf407df78ff4c4
4
- data.tar.gz: 33e3cc29078c68806f2cbf463945c11153c2e92d75bc8def9898a14c15623118
3
+ metadata.gz: 0afe2a59cbc6559cee74fe894ad0ed632b6788955377949f87c641a3550fff92
4
+ data.tar.gz: 1725783e695045041a65334679db26c2fb396f1e79a3c4d18d7491d91d479756
5
5
  SHA512:
6
- metadata.gz: ba6b7b8ad5f8715ad599cf4228a52970d4dd32834b5f35b35fc534ffc066e0b12e431e3ff16d9f30dbe4c5c88f49651e71d891f921bd8fd534a3276a1bf2a5dc
7
- data.tar.gz: 388f1a1bb1235012b25c336052b51e4a142c2d225af938cb7aa12e01580003ae7430e7a4e2c4b55f81508edc0547a4d6e7df05b2d9c31e636b5a49736cd48233
6
+ metadata.gz: 626069aef80ea5cf1b3254679fb7f7c9054fe7f908c464521e42c566186c21170f5a3187e98c59d6f1f209d107e53995facb58763965042ce257d2e4cd8f5d63
7
+ data.tar.gz: 0f941008d87d91b19bbd5829cf79580df7cabb9e3d37891aa3016fca9e4af844ecd8a30f3b4e6e567d6a47ece9249b6e6da02eda157928bd2ee8c5d45c6404d2
data/.rubocop.yml CHANGED
@@ -12,7 +12,7 @@ Layout/LineLength:
12
12
  - 'VCR\.use_cassette'
13
13
  - '(\A|\s)/.*?/'
14
14
  Metrics/BlockLength:
15
- ExcludedMethods: ['describe', 'context', 'before']
15
+ ExcludedMethods: ['describe', 'context', 'before', 'it']
16
16
  Metrics/MethodLength:
17
17
  Max: 30
18
18
  Metrics/ModuleLength:
data/Gemfile.lock CHANGED
@@ -1,7 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workos (5.6.0)
4
+ workos (5.8.0)
5
+ encryptor (~> 3.0)
6
+ jwt (~> 2.8)
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
@@ -9,12 +11,16 @@ GEM
9
11
  addressable (2.8.6)
10
12
  public_suffix (>= 2.0.2, < 6.0)
11
13
  ast (2.4.2)
14
+ base64 (0.2.0)
12
15
  bigdecimal (3.1.7)
13
16
  crack (1.0.0)
14
17
  bigdecimal
15
18
  rexml
16
19
  diff-lcs (1.5.1)
20
+ encryptor (3.0.0)
17
21
  hashdiff (1.1.0)
22
+ jwt (2.8.2)
23
+ base64
18
24
  parallel (1.24.0)
19
25
  parser (3.3.0.5)
20
26
  ast (~> 2.4.1)
@@ -6,10 +6,19 @@ module WorkOS
6
6
  class AuthenticationResponse
7
7
  include HashProvider
8
8
 
9
- attr_accessor :user, :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
@@ -9,7 +9,7 @@ module WorkOS
9
9
  class Profile
10
10
  include HashProvider
11
11
 
12
- attr_accessor :id, :email, :first_name, :last_name, :groups, :organization_id,
12
+ attr_accessor :id, :email, :first_name, :last_name, :role, :groups, :organization_id,
13
13
  :connection_id, :connection_type, :idp_id, :raw_attributes
14
14
 
15
15
  def initialize(profile_json)
@@ -19,6 +19,7 @@ module WorkOS
19
19
  @email = hash[:email]
20
20
  @first_name = hash[:first_name]
21
21
  @last_name = hash[:last_name]
22
+ @role = hash[:role]
22
23
  @groups = hash[:groups]
23
24
  @organization_id = hash[:organization_id]
24
25
  @connection_id = hash[:connection_id]
@@ -37,6 +38,7 @@ module WorkOS
37
38
  email: email,
38
39
  first_name: first_name,
39
40
  last_name: last_name,
41
+ role: role,
40
42
  groups: groups,
41
43
  organization_id: organization_id,
42
44
  connection_id: connection_id,
@@ -6,18 +6,41 @@ module WorkOS
6
6
  class RefreshAuthenticationResponse
7
7
  include HashProvider
8
8
 
9
- attr_accessor :access_token, :refresh_token
9
+ attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token, :sealed_session
10
10
 
11
- 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.8.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
@@ -302,6 +302,9 @@ describe WorkOS::SSO do
302
302
  id: 'prof_01EEJTY9SZ1R350RB7B73SNBKF',
303
303
  idp_id: '116485463307139932699',
304
304
  last_name: 'Loblaw',
305
+ role: {
306
+ slug: 'member',
307
+ },
305
308
  groups: nil,
306
309
  organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
307
310
  raw_attributes: {
@@ -373,6 +376,9 @@ describe WorkOS::SSO do
373
376
  id: 'prof_01DRA1XNSJDZ19A31F183ECQW5',
374
377
  idp_id: '00u1klkowm8EGah2H357',
375
378
  last_name: 'Demo',
379
+ role: {
380
+ slug: 'admin',
381
+ },
376
382
  groups: %w[Admins Developers],
377
383
  organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
378
384
  raw_attributes: {
@@ -467,6 +467,7 @@ describe WorkOS::UserManagement do
467
467
  )
468
468
  expect(authentication_response.access_token).to eq('<ACCESS_TOKEN>')
469
469
  expect(authentication_response.refresh_token).to eq('<REFRESH_TOKEN>')
470
+ expect(authentication_response.user.id).to eq('user_01H93WD0R0KWF8Q7BK02C0RPYJ')
470
471
  end
471
472
  end
472
473
  end
@@ -67,7 +67,7 @@ http_interactions:
67
67
  body:
68
68
  encoding: UTF-8
69
69
  string:
70
- '{"object":"profile","id":"prof_01EEJTY9SZ1R350RB7B73SNBKF","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01E83FVYZHY7DM4S9503JHV0R5","connection_type":"GoogleOAuth","idp_id":"116485463307139932699","email":"bob.loblaw@workos.com","first_name":"Bob","last_name":"Loblaw","raw_attributes":{"hd":"workos.com","id":"116485463307139932699","name":"Bob
70
+ '{"object":"profile","id":"prof_01EEJTY9SZ1R350RB7B73SNBKF","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01E83FVYZHY7DM4S9503JHV0R5","connection_type":"GoogleOAuth","idp_id":"116485463307139932699","email":"bob.loblaw@workos.com","first_name":"Bob","last_name":"Loblaw","role":{"slug":"member"},"raw_attributes":{"hd":"workos.com","id":"116485463307139932699","name":"Bob
71
71
  Loblaw","email":"bob.loblaw@workos.com","locale":"en","picture":"https://lh3.googleusercontent.com/a-/AOh14GyO2hLlgZvteDQ3Ldi3_-RteZLya0hWH7247Cam=s96-c","given_name":"Bob","family_name":"Loblaw","verified_email":true}}'
72
72
  http_version:
73
73
  recorded_at: Tue, 18 May 2021 22:55:21 GMT
@@ -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
@@ -1 +1 @@
1
- {"profile":{"object":"profile","id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01EMH8WAK20T42N2NBMNBCYHAG","connection_type":"OktaSAML","last_name":"Demo","groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357","raw_attributes":{"id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","last_name":"Demo","groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357"}},"access_token":"01DVX6QBS3EG6FHY2ESAA5Q65X"}
1
+ {"profile":{"object":"profile","id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","organization_id":"org_01FG53X8636WSNW2WEKB2C31ZB","connection_id":"conn_01EMH8WAK20T42N2NBMNBCYHAG","connection_type":"OktaSAML","last_name":"Demo","role":{"slug": "admin"},"groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357","raw_attributes":{"id":"prof_01DRA1XNSJDZ19A31F183ECQW5","email":"demo@workos-okta.com","first_name":"WorkOS","last_name":"Demo","groups":["Admins","Developers"],"idp_id":"00u1klkowm8EGah2H357"}},"access_token":"01DVX6QBS3EG6FHY2ESAA5Q65X"}
data/workos.gemspec CHANGED
@@ -21,6 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
22
  spec.require_paths = ['lib']
23
23
 
24
+ spec.add_dependency 'encryptor', '~> 3.0'
25
+ spec.add_dependency 'jwt', '~> 2.8'
26
+
24
27
  spec.add_development_dependency 'bundler', '>= 2.0.1'
25
28
  spec.add_development_dependency 'rspec', '~> 3.9.0'
26
29
  spec.add_development_dependency 'rubocop', '~> 0.77'
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workos
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0
4
+ version: 5.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WorkOS
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-19 00:00:00.000000000 Z
11
+ date: 2024-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: encryptor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.8'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +165,7 @@ files:
137
165
  - lib/workos/profile.rb
138
166
  - lib/workos/profile_and_token.rb
139
167
  - lib/workos/refresh_authentication_response.rb
168
+ - lib/workos/session.rb
140
169
  - lib/workos/sso.rb
141
170
  - lib/workos/types.rb
142
171
  - lib/workos/types/intent.rb
@@ -161,6 +190,7 @@ files:
161
190
  - spec/lib/workos/organizations_spec.rb
162
191
  - spec/lib/workos/passwordless_spec.rb
163
192
  - spec/lib/workos/portal_spec.rb
193
+ - spec/lib/workos/session_spec.rb
164
194
  - spec/lib/workos/sso_spec.rb
165
195
  - spec/lib/workos/user_management_spec.rb
166
196
  - spec/lib/workos/webhooks_spec.rb
@@ -373,6 +403,7 @@ test_files:
373
403
  - spec/lib/workos/organizations_spec.rb
374
404
  - spec/lib/workos/passwordless_spec.rb
375
405
  - spec/lib/workos/portal_spec.rb
406
+ - spec/lib/workos/session_spec.rb
376
407
  - spec/lib/workos/sso_spec.rb
377
408
  - spec/lib/workos/user_management_spec.rb
378
409
  - spec/lib/workos/webhooks_spec.rb