workos 5.31.1 → 6.0.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 +4 -4
- data/.github/workflows/version-bump.yml +1 -1
- data/Gemfile.lock +4 -4
- data/lib/workos/authentication_response.rb +11 -7
- data/lib/workos/encryptors/aes_gcm.rb +49 -0
- data/lib/workos/encryptors.rb +9 -0
- data/lib/workos/refresh_authentication_response.rb +11 -7
- data/lib/workos/session.rb +29 -32
- data/lib/workos/user_management.rb +3 -1
- data/lib/workos/version.rb +1 -1
- data/lib/workos.rb +1 -0
- data/spec/lib/workos/encryptors/aes_gcm_spec.rb +41 -0
- data/spec/lib/workos/session_spec.rb +88 -1
- data/workos.gemspec +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f0ac87a768fc79fa7ffbc77d0bf4a1ba30cbf6c1f5b96f8574c40bd2b61d296
|
|
4
|
+
data.tar.gz: eb05af387d9c1c40e5de4fb830e4ce83e7baf94a2d9bcb428d97032208688780
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f82d97362d7063dcc970313794671ca7f8c2c8892ae9728179e3c22a980b1d7e3d238959eb875d4a20629f13c00a56053347a70dfcb2214cf769979fc704151
|
|
7
|
+
data.tar.gz: 5954446fc2d3c6fbad2542c7d377bb7eb8361e514d1d099f01c052085aebc6952277fd15efe7e3cf9683fc2a9dbf4976240098d48e9305607ab3233978b23f21
|
|
@@ -67,7 +67,7 @@ jobs:
|
|
|
67
67
|
sed -i "s/VERSION = '.*'/VERSION = '${{ steps.bump-version.outputs.new_version }}'/" lib/workos/version.rb
|
|
68
68
|
|
|
69
69
|
- name: Create Pull Request
|
|
70
|
-
uses: peter-evans/create-pull-request@
|
|
70
|
+
uses: peter-evans/create-pull-request@v8
|
|
71
71
|
with:
|
|
72
72
|
token: ${{ steps.generate-token.outputs.token }}
|
|
73
73
|
commit-message: "v${{ steps.bump-version.outputs.new_version }}"
|
data/Gemfile.lock
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
workos (
|
|
4
|
+
workos (6.0.0)
|
|
5
5
|
encryptor (~> 3.0)
|
|
6
|
-
jwt (~>
|
|
6
|
+
jwt (~> 3.1)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
@@ -11,7 +11,7 @@ GEM
|
|
|
11
11
|
addressable (2.8.6)
|
|
12
12
|
public_suffix (>= 2.0.2, < 6.0)
|
|
13
13
|
ast (2.4.2)
|
|
14
|
-
base64 (0.
|
|
14
|
+
base64 (0.3.0)
|
|
15
15
|
bigdecimal (3.1.7)
|
|
16
16
|
crack (1.0.0)
|
|
17
17
|
bigdecimal
|
|
@@ -20,7 +20,7 @@ GEM
|
|
|
20
20
|
encryptor (3.0.0)
|
|
21
21
|
hashdiff (1.1.0)
|
|
22
22
|
json (2.9.1)
|
|
23
|
-
jwt (
|
|
23
|
+
jwt (3.1.2)
|
|
24
24
|
base64
|
|
25
25
|
language_server-protocol (3.17.0.3)
|
|
26
26
|
parallel (1.26.3)
|
|
@@ -31,13 +31,17 @@ module WorkOS
|
|
|
31
31
|
@oauth_tokens = json[:oauth_tokens] ? WorkOS::OAuthTokens.new(json[:oauth_tokens].to_json) : nil
|
|
32
32
|
@sealed_session =
|
|
33
33
|
if session && session[:seal_session]
|
|
34
|
-
WorkOS::Session.seal_data(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
WorkOS::Session.seal_data(
|
|
35
|
+
{
|
|
36
|
+
access_token: access_token,
|
|
37
|
+
refresh_token: refresh_token,
|
|
38
|
+
user: user.to_json,
|
|
39
|
+
organization_id: organization_id,
|
|
40
|
+
impersonator: impersonator.to_json,
|
|
41
|
+
},
|
|
42
|
+
session[:cookie_password],
|
|
43
|
+
encryptor: session[:encryptor],
|
|
44
|
+
)
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'encryptor'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'base64'
|
|
7
|
+
|
|
8
|
+
module WorkOS
|
|
9
|
+
module Encryptors
|
|
10
|
+
# Default encryptor using AES-256-GCM.
|
|
11
|
+
# Implements the encryptor interface: #seal(data, key) and #unseal(sealed_data, key)
|
|
12
|
+
class AesGcm
|
|
13
|
+
# Encrypts and seals data using AES-256-GCM
|
|
14
|
+
# @param data [Hash] The data to seal
|
|
15
|
+
# @param key [String] The encryption key
|
|
16
|
+
# @return [String] Base64-encoded sealed data
|
|
17
|
+
def seal(data, key)
|
|
18
|
+
iv = SecureRandom.random_bytes(12)
|
|
19
|
+
|
|
20
|
+
encrypted_data = Encryptor.encrypt(
|
|
21
|
+
value: JSON.generate(data),
|
|
22
|
+
key: key,
|
|
23
|
+
iv: iv,
|
|
24
|
+
algorithm: 'aes-256-gcm',
|
|
25
|
+
)
|
|
26
|
+
Base64.encode64(iv + encrypted_data)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Decrypts and unseals data using AES-256-GCM
|
|
30
|
+
# @param sealed_data [String] The sealed data to unseal
|
|
31
|
+
# @param key [String] The decryption key
|
|
32
|
+
# @return [Hash] The unsealed data with symbolized keys
|
|
33
|
+
def unseal(sealed_data, key)
|
|
34
|
+
decoded_data = Base64.decode64(sealed_data)
|
|
35
|
+
iv = decoded_data[0..11]
|
|
36
|
+
encrypted_data = decoded_data[12..]
|
|
37
|
+
|
|
38
|
+
decrypted_data = Encryptor.decrypt(
|
|
39
|
+
value: encrypted_data,
|
|
40
|
+
key: key,
|
|
41
|
+
iv: iv,
|
|
42
|
+
algorithm: 'aes-256-gcm',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
JSON.parse(decrypted_data, symbolize_names: true)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WorkOS
|
|
4
|
+
# Encryptors module provides pluggable encryption implementations for session data.
|
|
5
|
+
# The default encryptor is AesGcm, which uses AES-256-GCM encryption.
|
|
6
|
+
module Encryptors
|
|
7
|
+
autoload :AesGcm, 'workos/encryptors/aes_gcm'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -22,13 +22,17 @@ module WorkOS
|
|
|
22
22
|
end
|
|
23
23
|
@sealed_session =
|
|
24
24
|
if session && session[:seal_session]
|
|
25
|
-
WorkOS::Session.seal_data(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
WorkOS::Session.seal_data(
|
|
26
|
+
{
|
|
27
|
+
access_token: access_token,
|
|
28
|
+
refresh_token: refresh_token,
|
|
29
|
+
user: user.to_json,
|
|
30
|
+
organization_id: organization_id,
|
|
31
|
+
impersonator: impersonator.to_json,
|
|
32
|
+
},
|
|
33
|
+
session[:cookie_password],
|
|
34
|
+
encryptor: session[:encryptor],
|
|
35
|
+
)
|
|
32
36
|
end
|
|
33
37
|
end
|
|
34
38
|
# rubocop:enable Metrics/AbcSize
|
data/lib/workos/session.rb
CHANGED
|
@@ -12,11 +12,14 @@ module WorkOS
|
|
|
12
12
|
# The Session class provides helper methods for working with WorkOS sessions
|
|
13
13
|
# This class is not meant to be instantiated in a user space, and is instantiated internally but exposed.
|
|
14
14
|
class Session
|
|
15
|
-
attr_accessor :jwks, :jwks_algorithms, :user_management, :cookie_password, :session_data, :client_id
|
|
15
|
+
attr_accessor :jwks, :jwks_algorithms, :user_management, :cookie_password, :session_data, :client_id, :encryptor
|
|
16
16
|
|
|
17
|
-
def initialize(user_management:, client_id:, session_data:, cookie_password:)
|
|
17
|
+
def initialize(user_management:, client_id:, session_data:, cookie_password:, encryptor: nil)
|
|
18
18
|
raise ArgumentError, 'cookiePassword is required' if cookie_password.nil? || cookie_password.empty?
|
|
19
19
|
|
|
20
|
+
@encryptor = encryptor || WorkOS::Encryptors::AesGcm.new
|
|
21
|
+
validate_encryptor!(@encryptor)
|
|
22
|
+
|
|
20
23
|
@user_management = user_management
|
|
21
24
|
@cookie_password = cookie_password
|
|
22
25
|
@session_data = session_data
|
|
@@ -30,13 +33,14 @@ module WorkOS
|
|
|
30
33
|
|
|
31
34
|
# Authenticates the user based on the session data
|
|
32
35
|
# @param include_expired [Boolean] If true, returns decoded token data even when expired (default: false)
|
|
36
|
+
# @param block [Proc] Optional block to call to extract additional claims from the decoded JWT
|
|
33
37
|
# @return [Hash] A hash containing the authentication response and a reason if the authentication failed
|
|
34
38
|
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
35
|
-
def authenticate(include_expired: false)
|
|
39
|
+
def authenticate(include_expired: false, &claim_extractor)
|
|
36
40
|
return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil?
|
|
37
41
|
|
|
38
42
|
begin
|
|
39
|
-
session = Session.unseal_data(@session_data, @cookie_password)
|
|
43
|
+
session = Session.unseal_data(@session_data, @cookie_password, encryptor: @encryptor)
|
|
40
44
|
rescue StandardError
|
|
41
45
|
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
|
|
42
46
|
end
|
|
@@ -59,7 +63,7 @@ module WorkOS
|
|
|
59
63
|
return { authenticated: false, reason: 'INVALID_JWT' } if expired && !include_expired
|
|
60
64
|
|
|
61
65
|
# Return full data for valid tokens or when include_expired is true
|
|
62
|
-
{
|
|
66
|
+
result = {
|
|
63
67
|
authenticated: !expired,
|
|
64
68
|
session_id: decoded['sid'],
|
|
65
69
|
organization_id: decoded['org_id'],
|
|
@@ -72,6 +76,8 @@ module WorkOS
|
|
|
72
76
|
impersonator: session[:impersonator],
|
|
73
77
|
reason: expired ? 'INVALID_JWT' : nil,
|
|
74
78
|
}
|
|
79
|
+
result.merge!(claim_extractor.call(decoded)) if block_given?
|
|
80
|
+
result
|
|
75
81
|
rescue JWT::DecodeError
|
|
76
82
|
{ authenticated: false, reason: 'INVALID_JWT' }
|
|
77
83
|
rescue StandardError => e
|
|
@@ -89,7 +95,7 @@ module WorkOS
|
|
|
89
95
|
cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]
|
|
90
96
|
|
|
91
97
|
begin
|
|
92
|
-
session = Session.unseal_data(@session_data, cookie_password)
|
|
98
|
+
session = Session.unseal_data(@session_data, cookie_password, encryptor: @encryptor)
|
|
93
99
|
rescue StandardError
|
|
94
100
|
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
|
|
95
101
|
end
|
|
@@ -101,7 +107,7 @@ module WorkOS
|
|
|
101
107
|
client_id: @client_id,
|
|
102
108
|
refresh_token: session[:refresh_token],
|
|
103
109
|
organization_id: options.nil? || options[:organization_id].nil? ? nil : options[:organization_id],
|
|
104
|
-
session: { seal_session: true, cookie_password: cookie_password },
|
|
110
|
+
session: { seal_session: true, cookie_password: cookie_password, encryptor: @encryptor },
|
|
105
111
|
)
|
|
106
112
|
|
|
107
113
|
@session_data = auth_response.sealed_session
|
|
@@ -134,43 +140,34 @@ module WorkOS
|
|
|
134
140
|
@user_management.get_logout_url(session_id: auth_response[:session_id], return_to: return_to)
|
|
135
141
|
end
|
|
136
142
|
|
|
137
|
-
# Encrypts and seals data using AES-256-GCM
|
|
143
|
+
# Encrypts and seals data using the provided encryptor (defaults to AES-256-GCM)
|
|
138
144
|
# @param data [Hash] The data to seal
|
|
139
145
|
# @param key [String] The key to use for encryption
|
|
146
|
+
# @param encryptor [Object] Optional encryptor that responds to #seal(data, key)
|
|
140
147
|
# @return [String] The sealed data
|
|
141
|
-
def self.seal_data(data, key)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
encrypted_data = Encryptor.encrypt(
|
|
145
|
-
value: JSON.generate(data),
|
|
146
|
-
key: key,
|
|
147
|
-
iv: iv,
|
|
148
|
-
algorithm: 'aes-256-gcm',
|
|
149
|
-
)
|
|
150
|
-
Base64.encode64(iv + encrypted_data) # Combine IV with encrypted data and encode as base64
|
|
148
|
+
def self.seal_data(data, key, encryptor: nil)
|
|
149
|
+
enc = encryptor || WorkOS::Encryptors::AesGcm.new
|
|
150
|
+
enc.seal(data, key)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
# Decrypts and unseals data using AES-256-GCM
|
|
153
|
+
# Decrypts and unseals data using the provided encryptor (defaults to AES-256-GCM)
|
|
154
154
|
# @param sealed_data [String] The sealed data to unseal
|
|
155
155
|
# @param key [String] The key to use for decryption
|
|
156
|
+
# @param encryptor [Object] Optional encryptor that responds to #unseal(sealed_data, key)
|
|
156
157
|
# @return [Hash] The unsealed data
|
|
157
|
-
def self.unseal_data(sealed_data, key)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
encrypted_data = decoded_data[12..-1] # Extract the encrypted data
|
|
161
|
-
|
|
162
|
-
decrypted_data = Encryptor.decrypt(
|
|
163
|
-
value: encrypted_data,
|
|
164
|
-
key: key,
|
|
165
|
-
iv: iv,
|
|
166
|
-
algorithm: 'aes-256-gcm',
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
JSON.parse(decrypted_data, symbolize_names: true) # Parse the decrypted JSON string back to original data
|
|
158
|
+
def self.unseal_data(sealed_data, key, encryptor: nil)
|
|
159
|
+
enc = encryptor || WorkOS::Encryptors::AesGcm.new
|
|
160
|
+
enc.unseal(sealed_data, key)
|
|
170
161
|
end
|
|
171
162
|
|
|
172
163
|
private
|
|
173
164
|
|
|
165
|
+
def validate_encryptor!(enc)
|
|
166
|
+
return if enc.respond_to?(:seal) && enc.respond_to?(:unseal)
|
|
167
|
+
|
|
168
|
+
raise ArgumentError, 'encryptor must respond to #seal(data, key) and #unseal(sealed_data, key)'
|
|
169
|
+
end
|
|
170
|
+
|
|
174
171
|
# Creates a JWKS set from a remote JWKS URL
|
|
175
172
|
# @param uri [URI] The URI of the JWKS
|
|
176
173
|
# @return [JWT::JWK::Set] The JWKS set
|
|
@@ -42,14 +42,16 @@ module WorkOS
|
|
|
42
42
|
# @param [String] client_id The WorkOS client ID for the environment
|
|
43
43
|
# @param [String] session_data The sealed session data
|
|
44
44
|
# @param [String] cookie_password The password used to seal the session
|
|
45
|
+
# @param [Object] encryptor Optional custom encryptor that responds to #seal and #unseal
|
|
45
46
|
#
|
|
46
47
|
# @return WorkOS::Session
|
|
47
|
-
def load_sealed_session(client_id:, session_data:, cookie_password:)
|
|
48
|
+
def load_sealed_session(client_id:, session_data:, cookie_password:, encryptor: nil)
|
|
48
49
|
WorkOS::Session.new(
|
|
49
50
|
user_management: self,
|
|
50
51
|
client_id: client_id,
|
|
51
52
|
session_data: session_data,
|
|
52
53
|
cookie_password: cookie_password,
|
|
54
|
+
encryptor: encryptor,
|
|
53
55
|
)
|
|
54
56
|
end
|
|
55
57
|
|
data/lib/workos/version.rb
CHANGED
data/lib/workos.rb
CHANGED
|
@@ -56,6 +56,7 @@ module WorkOS
|
|
|
56
56
|
autoload :DirectorySync, 'workos/directory_sync'
|
|
57
57
|
autoload :DirectoryUser, 'workos/directory_user'
|
|
58
58
|
autoload :EmailVerification, 'workos/email_verification'
|
|
59
|
+
autoload :Encryptors, 'workos/encryptors'
|
|
59
60
|
autoload :Event, 'workos/event'
|
|
60
61
|
autoload :Events, 'workos/events'
|
|
61
62
|
autoload :Factor, 'workos/factor'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe WorkOS::Encryptors::AesGcm do
|
|
4
|
+
subject(:encryptor) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:key) { 'a' * 32 }
|
|
7
|
+
let(:data) { { access_token: 'tok_123', user: { id: 'user_01' } } }
|
|
8
|
+
|
|
9
|
+
describe '#seal' do
|
|
10
|
+
it 'returns a base64-encoded string' do
|
|
11
|
+
sealed = encryptor.seal(data, key)
|
|
12
|
+
expect(sealed).to be_a(String)
|
|
13
|
+
expect { Base64.decode64(sealed) }.not_to raise_error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'produces different output each time (random IV)' do
|
|
17
|
+
sealed1 = encryptor.seal(data, key)
|
|
18
|
+
sealed2 = encryptor.seal(data, key)
|
|
19
|
+
expect(sealed1).not_to eq(sealed2)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#unseal' do
|
|
24
|
+
it 'round-trips data correctly' do
|
|
25
|
+
sealed = encryptor.seal(data, key)
|
|
26
|
+
unsealed = encryptor.unseal(sealed, key)
|
|
27
|
+
expect(unsealed).to eq(data)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'returns hash with symbolized keys' do
|
|
31
|
+
sealed = encryptor.seal({ 'string_key' => 'value' }, key)
|
|
32
|
+
unsealed = encryptor.unseal(sealed, key)
|
|
33
|
+
expect(unsealed.keys.first).to be_a(Symbol)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'raises error with wrong key' do
|
|
37
|
+
sealed = encryptor.seal(data, key)
|
|
38
|
+
expect { encryptor.unseal(sealed, 'b' * 32) }.to raise_error(OpenSSL::Cipher::CipherError)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -5,8 +5,8 @@ describe WorkOS::Session do
|
|
|
5
5
|
let(:cookie_password) { 'test_very_long_cookie_password__' }
|
|
6
6
|
let(:session_data) { 'test_session_data' }
|
|
7
7
|
let(:jwks_url) { 'https://api.workos.com/sso/jwks/client_123' }
|
|
8
|
-
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
|
|
9
8
|
let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), { kid: 'sso_oidc_key_pair_123', use: 'sig', alg: 'RS256' }) }
|
|
9
|
+
let(:jwks_hash) { { keys: [jwk.export] }.to_json }
|
|
10
10
|
|
|
11
11
|
before do
|
|
12
12
|
allow(Net::HTTP).to receive(:get).and_return(jwks_hash)
|
|
@@ -222,6 +222,29 @@ describe WorkOS::Session do
|
|
|
222
222
|
})
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
+
it 'merges custom claims from claim_extractor block' do
|
|
226
|
+
custom_payload = payload.merge(custom_claim: 'custom_value', another_claim: 123)
|
|
227
|
+
custom_access_token = JWT.encode(custom_payload, jwk.signing_key, jwk[:alg], { kid: jwk[:kid] })
|
|
228
|
+
custom_session_data = WorkOS::Session.seal_data({
|
|
229
|
+
access_token: custom_access_token,
|
|
230
|
+
user: 'user',
|
|
231
|
+
impersonator: 'impersonator',
|
|
232
|
+
}, cookie_password,)
|
|
233
|
+
session = WorkOS::Session.new(
|
|
234
|
+
user_management: user_management,
|
|
235
|
+
client_id: client_id,
|
|
236
|
+
session_data: custom_session_data,
|
|
237
|
+
cookie_password: cookie_password,
|
|
238
|
+
)
|
|
239
|
+
allow_any_instance_of(JWT::Decode).to receive(:verify_signature).and_return(true)
|
|
240
|
+
result = session.authenticate do |jwt|
|
|
241
|
+
{ my_custom_claim: jwt['custom_claim'], my_other_claim: jwt['another_claim'] }
|
|
242
|
+
end
|
|
243
|
+
expect(result[:authenticated]).to be true
|
|
244
|
+
expect(result[:my_custom_claim]).to eq('custom_value')
|
|
245
|
+
expect(result[:my_other_claim]).to eq(123)
|
|
246
|
+
end
|
|
247
|
+
|
|
225
248
|
describe 'with entitlements' do
|
|
226
249
|
let(:payload) do
|
|
227
250
|
{
|
|
@@ -385,4 +408,68 @@ describe WorkOS::Session do
|
|
|
385
408
|
end
|
|
386
409
|
end
|
|
387
410
|
end
|
|
411
|
+
|
|
412
|
+
describe 'custom encryptor' do
|
|
413
|
+
let(:user_management) { instance_double('UserManagement') }
|
|
414
|
+
let(:custom_encryptor) do
|
|
415
|
+
Class.new do
|
|
416
|
+
def seal(data, _key)
|
|
417
|
+
"CUSTOM:#{JSON.generate(data)}"
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def unseal(sealed_data, _key)
|
|
421
|
+
json = sealed_data.sub('CUSTOM:', '')
|
|
422
|
+
JSON.parse(json, symbolize_names: true)
|
|
423
|
+
end
|
|
424
|
+
end.new
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
before do
|
|
428
|
+
allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it 'uses custom encryptor for seal_data' do
|
|
432
|
+
sealed = WorkOS::Session.seal_data({ foo: 'bar' }, 'key', encryptor: custom_encryptor)
|
|
433
|
+
expect(sealed).to start_with('CUSTOM:')
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
it 'uses custom encryptor for unseal_data' do
|
|
437
|
+
sealed = 'CUSTOM:{"foo":"bar"}'
|
|
438
|
+
unsealed = WorkOS::Session.unseal_data(sealed, 'key', encryptor: custom_encryptor)
|
|
439
|
+
expect(unsealed).to eq({ foo: 'bar' })
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it 'accepts custom encryptor in initialize' do
|
|
443
|
+
session = WorkOS::Session.new(
|
|
444
|
+
user_management: user_management,
|
|
445
|
+
client_id: client_id,
|
|
446
|
+
session_data: session_data,
|
|
447
|
+
cookie_password: cookie_password,
|
|
448
|
+
encryptor: custom_encryptor,
|
|
449
|
+
)
|
|
450
|
+
expect(session.encryptor).to eq(custom_encryptor)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it 'defaults to AesGcm encryptor when none provided' do
|
|
454
|
+
session = WorkOS::Session.new(
|
|
455
|
+
user_management: user_management,
|
|
456
|
+
client_id: client_id,
|
|
457
|
+
session_data: session_data,
|
|
458
|
+
cookie_password: cookie_password,
|
|
459
|
+
)
|
|
460
|
+
expect(session.encryptor).to be_a(WorkOS::Encryptors::AesGcm)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it 'raises ArgumentError for invalid encryptor' do
|
|
464
|
+
expect do
|
|
465
|
+
WorkOS::Session.new(
|
|
466
|
+
user_management: user_management,
|
|
467
|
+
client_id: client_id,
|
|
468
|
+
session_data: session_data,
|
|
469
|
+
cookie_password: cookie_password,
|
|
470
|
+
encryptor: Object.new,
|
|
471
|
+
)
|
|
472
|
+
end.to raise_error(ArgumentError, /must respond to/)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
388
475
|
end
|
data/workos.gemspec
CHANGED
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.require_paths = ['lib']
|
|
23
23
|
|
|
24
24
|
spec.add_dependency 'encryptor', '~> 3.0'
|
|
25
|
-
spec.add_dependency 'jwt', '~>
|
|
25
|
+
spec.add_dependency 'jwt', '~> 3.1'
|
|
26
26
|
|
|
27
27
|
spec.add_development_dependency 'bundler', '>= 2.0.1'
|
|
28
28
|
spec.add_development_dependency 'rake'
|
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:
|
|
4
|
+
version: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- WorkOS
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: encryptor
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '3.1'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '3.1'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: bundler
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,6 +166,8 @@ files:
|
|
|
166
166
|
- lib/workos/directory_sync.rb
|
|
167
167
|
- lib/workos/directory_user.rb
|
|
168
168
|
- lib/workos/email_verification.rb
|
|
169
|
+
- lib/workos/encryptors.rb
|
|
170
|
+
- lib/workos/encryptors/aes_gcm.rb
|
|
169
171
|
- lib/workos/errors.rb
|
|
170
172
|
- lib/workos/event.rb
|
|
171
173
|
- lib/workos/events.rb
|
|
@@ -211,6 +213,7 @@ files:
|
|
|
211
213
|
- spec/lib/workos/configuration_spec.rb
|
|
212
214
|
- spec/lib/workos/directory_sync_spec.rb
|
|
213
215
|
- spec/lib/workos/directory_user_spec.rb
|
|
216
|
+
- spec/lib/workos/encryptors/aes_gcm_spec.rb
|
|
214
217
|
- spec/lib/workos/event_spec.rb
|
|
215
218
|
- spec/lib/workos/mfa_spec.rb
|
|
216
219
|
- spec/lib/workos/organizations_spec.rb
|
|
@@ -451,6 +454,7 @@ test_files:
|
|
|
451
454
|
- spec/lib/workos/configuration_spec.rb
|
|
452
455
|
- spec/lib/workos/directory_sync_spec.rb
|
|
453
456
|
- spec/lib/workos/directory_user_spec.rb
|
|
457
|
+
- spec/lib/workos/encryptors/aes_gcm_spec.rb
|
|
454
458
|
- spec/lib/workos/event_spec.rb
|
|
455
459
|
- spec/lib/workos/mfa_spec.rb
|
|
456
460
|
- spec/lib/workos/organizations_spec.rb
|