workos 5.31.1 → 6.1.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: a5e37c10b9ae34685d2545d670ca866dbda73c22df75fc1a2e8d79c159d3a2c7
4
- data.tar.gz: a6f6e36847db549930e7a18ae58e9e95a9c249eb7a93e4466f14109e50e7fddd
3
+ metadata.gz: 2c2b3e2f56e5f5ffc2d59d27eb6613c018b24ec57ff270ed9e95730a46aafdc6
4
+ data.tar.gz: 6e001f5af6046daf9b640f570cd7318767fc106e90cdf3c9e7f8a3b0c11d1b3b
5
5
  SHA512:
6
- metadata.gz: 37ce63e77ccf9dd05b0d85837c5e1334ae155cd9beb0500d9b3faa3dd985c9638f54f77ff5cd1f350e3d283db23ac16f7a08bd972f308852a6f5eb4c63ebfa79
7
- data.tar.gz: 77b058f1b59efe3a390817173d8a4b45557d41f95385196af8c999e4130ddc83a7a306b68714a7c5c2f7cc695db2e21192599792bce156918bce435a0b2d9b7f
6
+ metadata.gz: aa0bdfff2eb4de65c9bd5679be9f44b36df9eafd5eb8d3826f490d435825f61f3df32768e77b8954df538a3e3af2963aaba3fe23c89148b7f7619d83ea349c46
7
+ data.tar.gz: 6ae668143797a324cea0db2d9d779fe803c13f9defc62d707bb7b4419ba931a71d12ff5dd4bdc17da5c6c284623b1484a111817a1294e1f02f0e1e741dd15781
@@ -0,0 +1,20 @@
1
+ name: Lint PR Title
2
+
3
+ on:
4
+ pull_request_target:
5
+ types:
6
+ - opened
7
+ - edited
8
+ - synchronize
9
+
10
+ permissions:
11
+ pull-requests: read
12
+
13
+ jobs:
14
+ main:
15
+ name: Validate PR title
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: amannn/action-semantic-pull-request@v6
19
+ env:
20
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,25 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-please:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Generate token
17
+ id: generate-token
18
+ uses: actions/create-github-app-token@v2
19
+ with:
20
+ app-id: ${{ vars.SDK_BOT_APP_ID }}
21
+ private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
22
+
23
+ - uses: googleapis/release-please-action@v4
24
+ with:
25
+ token: ${{ steps.generate-token.outputs.token }}
@@ -1,53 +1,16 @@
1
1
  name: Release
2
2
 
3
3
  on:
4
- pull_request:
5
- types: [closed]
6
- branches: [main]
4
+ release:
5
+ types: [published]
7
6
 
8
7
  defaults:
9
8
  run:
10
9
  shell: bash
11
10
 
12
11
  jobs:
13
- create-release:
14
- name: Create GitHub Release
15
- if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'version-bump')
16
- runs-on: ubuntu-latest
17
- permissions:
18
- contents: write
19
- outputs:
20
- version: ${{ steps.get-version.outputs.version }}
21
- steps:
22
- - name: Generate token
23
- id: generate-token
24
- uses: actions/create-github-app-token@v2
25
- with:
26
- app-id: ${{ vars.SDK_BOT_APP_ID }}
27
- private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
28
-
29
- - name: Checkout
30
- uses: actions/checkout@v6
31
- with:
32
- token: ${{ steps.generate-token.outputs.token }}
33
-
34
- - name: Get version from version.rb
35
- id: get-version
36
- run: |
37
- VERSION=$(grep "VERSION = " lib/workos/version.rb | sed "s/.*VERSION = '\(.*\)'/\1/")
38
- echo "version=$VERSION" >> $GITHUB_OUTPUT
39
-
40
- - name: Create Release
41
- uses: softprops/action-gh-release@v2
42
- with:
43
- tag_name: v${{ steps.get-version.outputs.version }}
44
- name: v${{ steps.get-version.outputs.version }}
45
- generate_release_notes: true
46
- token: ${{ steps.generate-token.outputs.token }}
47
-
48
12
  publish:
49
13
  name: Publish to RubyGems
50
- needs: create-release
51
14
  runs-on: ubuntu-latest
52
15
  permissions:
53
16
  id-token: write
@@ -72,5 +35,6 @@ jobs:
72
35
 
73
36
  - name: Publish to RubyGems
74
37
  run: |
38
+ VERSION="${GITHUB_REF_NAME#v}"
75
39
  bundle exec rake build
76
- gem push pkg/workos-${{ needs.create-release.outputs.version }}.gem --host https://rubygems.org
40
+ gem push pkg/workos-${VERSION}.gem --host https://rubygems.org
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "6.1.0"
3
+ }
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [6.1.0](https://github.com/workos/workos-ruby/compare/workos-v6.0.0...workos/v6.1.0) (2026-02-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * add support for totp_secret ([#300](https://github.com/workos/workos-ruby/issues/300)) ([c0a26bf](https://github.com/workos/workos-ruby/commit/c0a26bf745fb49ebaac7c5241e99d51188b886bb))
9
+ * Include Feature Flags decoded from the JWT in the payload of a Session ([#386](https://github.com/workos/workos-ruby/issues/386)) ([31a0e79](https://github.com/workos/workos-ruby/commit/31a0e7901247652182dcaad95e131357b93d0d71))
10
+ * **workos-ruby:** Add `connection` to `authorization_url` ([#78](https://github.com/workos/workos-ruby/issues/78)) ([c3a0e8e](https://github.com/workos/workos-ruby/commit/c3a0e8e4031a3ee888d925c11f1fd2fb152f0a16))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * add `invitation_token` parameter to authentication methods ([#438](https://github.com/workos/workos-ruby/issues/438)) ([d24e3dc](https://github.com/workos/workos-ruby/commit/d24e3dc2995de26970415e4570a7ed810d432715))
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workos (5.31.1)
4
+ workos (6.1.0)
5
5
  encryptor (~> 3.0)
6
- jwt (~> 2.8)
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.2.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 (2.10.2)
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
- access_token: access_token,
36
- refresh_token: refresh_token,
37
- user: user.to_json,
38
- organization_id: organization_id,
39
- impersonator: impersonator.to_json,
40
- }, session[:cookie_password],)
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
- 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],)
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
@@ -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
- iv = SecureRandom.random_bytes(12)
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
- decoded_data = Base64.decode64(sealed_data)
159
- iv = decoded_data[0..11] # Extract the IV (first 12 bytes)
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
 
@@ -296,16 +298,19 @@ module WorkOS
296
298
  # @param [String] client_id The WorkOS client ID for the environment
297
299
  # @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
298
300
  # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
301
+ # @param [String] invitation_token The token of an Invitation, if required.
299
302
  # @param [Hash] session An optional hash that determines whether the session should be sealed and
300
303
  # the optional cookie password.
301
304
  #
302
305
  # @return WorkOS::AuthenticationResponse
306
+ # rubocop:disable Metrics/ParameterLists
303
307
  def authenticate_with_password(
304
308
  email:,
305
309
  password:,
306
310
  client_id:,
307
311
  ip_address: nil,
308
312
  user_agent: nil,
313
+ invitation_token: nil,
309
314
  session: nil
310
315
  )
311
316
  validate_session(session)
@@ -320,6 +325,7 @@ module WorkOS
320
325
  password: password,
321
326
  ip_address: ip_address,
322
327
  user_agent: user_agent,
328
+ invitation_token: invitation_token,
323
329
  grant_type: 'password',
324
330
  },
325
331
  ),
@@ -327,6 +333,7 @@ module WorkOS
327
333
 
328
334
  WorkOS::AuthenticationResponse.new(response.body, session)
329
335
  end
336
+ # rubocop:enable Metrics/ParameterLists
330
337
 
331
338
  # Authenticate a user using OAuth or an organization's SSO connection.
332
339
  #
@@ -335,6 +342,7 @@ module WorkOS
335
342
  # @param [String] client_id The WorkOS client ID for the environment
336
343
  # @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
337
344
  # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
345
+ # @param [String] invitation_token The token of an Invitation, if required.
338
346
  # @param [Hash] session An optional hash that determines whether the session should be sealed and
339
347
  # the optional cookie password.
340
348
  #
@@ -344,6 +352,7 @@ module WorkOS
344
352
  client_id:,
345
353
  ip_address: nil,
346
354
  user_agent: nil,
355
+ invitation_token: nil,
347
356
  session: nil
348
357
  )
349
358
  validate_session(session)
@@ -357,6 +366,7 @@ module WorkOS
357
366
  client_secret: WorkOS.config.key!,
358
367
  ip_address: ip_address,
359
368
  user_agent: user_agent,
369
+ invitation_token: invitation_token,
360
370
  grant_type: 'authorization_code',
361
371
  },
362
372
  ),
@@ -413,6 +423,7 @@ module WorkOS
413
423
  # @param [String] link_authorization_code Used to link an OAuth profile to an existing user,
414
424
  # after having completed a Magic Code challenge.
415
425
  # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
426
+ # @param [String] invitation_token The token of an Invitation, if required.
416
427
  # @param [Hash] session An optional hash that determines whether the session should be sealed and
417
428
  # the optional cookie password.
418
429
  #
@@ -425,6 +436,7 @@ module WorkOS
425
436
  ip_address: nil,
426
437
  user_agent: nil,
427
438
  link_authorization_code: nil,
439
+ invitation_token: nil,
428
440
  session: nil
429
441
  )
430
442
  validate_session(session)
@@ -441,6 +453,7 @@ module WorkOS
441
453
  user_agent: user_agent,
442
454
  grant_type: 'urn:workos:oauth:grant-type:magic-auth:code',
443
455
  link_authorization_code: link_authorization_code,
456
+ invitation_token: invitation_token,
444
457
  },
445
458
  ),
446
459
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WorkOS
4
- VERSION = '5.31.1'
4
+ VERSION = '6.1.0'
5
5
  end
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,12 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "ruby",
6
+ "package-name": "workos",
7
+ "version-file": "lib/workos/version.rb",
8
+ "changelog-path": "CHANGELOG.md",
9
+ "include-component-in-tag": false
10
+ }
11
+ }
12
+ }
@@ -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
@@ -588,6 +588,28 @@ describe WorkOS::UserManagement do
588
588
  end
589
589
  end
590
590
  end
591
+
592
+ context 'with an invitation_token' do
593
+ it 'includes invitation_token in the request body' do
594
+ expect(described_class).to receive(:post_request) do |options|
595
+ body = options[:body]
596
+ expect(body[:invitation_token]).to eq('invitation_token_123')
597
+
598
+ double('request')
599
+ end.and_return(double('request'))
600
+
601
+ expect(described_class).to receive(:execute_request).and_return(
602
+ double('response', body: '{"user": {"id": "user_123"}, "access_token": "token", "refresh_token": "refresh"}'),
603
+ )
604
+
605
+ described_class.authenticate_with_password(
606
+ email: 'test@workos.app',
607
+ password: 'password123',
608
+ client_id: 'client_123',
609
+ invitation_token: 'invitation_token_123',
610
+ )
611
+ end
612
+ end
591
613
  end
592
614
 
593
615
  describe '.authenticate_with_code' do
@@ -671,6 +693,27 @@ describe WorkOS::UserManagement do
671
693
  end
672
694
  end
673
695
  end
696
+
697
+ context 'with an invitation_token' do
698
+ it 'includes invitation_token in the request body' do
699
+ expect(described_class).to receive(:post_request) do |options|
700
+ body = options[:body]
701
+ expect(body[:invitation_token]).to eq('invitation_token_123')
702
+
703
+ double('request')
704
+ end.and_return(double('request'))
705
+
706
+ expect(described_class).to receive(:execute_request).and_return(
707
+ double('response', body: '{"user": {"id": "user_123"}, "access_token": "token", "refresh_token": "refresh"}'),
708
+ )
709
+
710
+ described_class.authenticate_with_code(
711
+ code: '01H93ZZHA0JBHFJH9RR11S83YN',
712
+ client_id: 'client_123',
713
+ invitation_token: 'invitation_token_123',
714
+ )
715
+ end
716
+ end
674
717
  end
675
718
 
676
719
  describe '.authenticate_with_refresh_token' do
@@ -735,6 +778,28 @@ describe WorkOS::UserManagement do
735
778
  end
736
779
  end
737
780
  end
781
+
782
+ context 'with an invitation_token' do
783
+ it 'includes invitation_token in the request body' do
784
+ expect(described_class).to receive(:post_request) do |options|
785
+ body = options[:body]
786
+ expect(body[:invitation_token]).to eq('invitation_token_123')
787
+
788
+ double('request')
789
+ end.and_return(double('request'))
790
+
791
+ expect(described_class).to receive(:execute_request).and_return(
792
+ double('response', body: '{"user": {"id": "user_123"}, "access_token": "token", "refresh_token": "refresh"}'),
793
+ )
794
+
795
+ described_class.authenticate_with_magic_auth(
796
+ code: '452079',
797
+ client_id: 'client_123',
798
+ email: 'test@workos.com',
799
+ invitation_token: 'invitation_token_123',
800
+ )
801
+ end
802
+ end
738
803
  end
739
804
 
740
805
  describe '.authenticate_with_organization_selection' do
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', '~> 2.8'
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: 5.31.1
4
+ version: 6.1.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-03 00:00:00.000000000 Z
11
+ date: 2026-02-10 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: '2.8'
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: '2.8'
40
+ version: '3.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -133,13 +133,16 @@ files:
133
133
  - ".github/pull_request_template.md"
134
134
  - ".github/renovate.json"
135
135
  - ".github/workflows/ci.yml"
136
+ - ".github/workflows/lint-pr-title.yml"
137
+ - ".github/workflows/release-please.yml"
136
138
  - ".github/workflows/release.yml"
137
- - ".github/workflows/version-bump.yml"
138
139
  - ".gitignore"
140
+ - ".release-please-manifest.json"
139
141
  - ".rspec"
140
142
  - ".rubocop.yml"
141
143
  - ".rubocop_todo.yml"
142
144
  - ".ruby-version"
145
+ - CHANGELOG.md
143
146
  - Gemfile
144
147
  - Gemfile.lock
145
148
  - LICENSE
@@ -166,6 +169,8 @@ files:
166
169
  - lib/workos/directory_sync.rb
167
170
  - lib/workos/directory_user.rb
168
171
  - lib/workos/email_verification.rb
172
+ - lib/workos/encryptors.rb
173
+ - lib/workos/encryptors/aes_gcm.rb
169
174
  - lib/workos/errors.rb
170
175
  - lib/workos/event.rb
171
176
  - lib/workos/events.rb
@@ -205,12 +210,14 @@ files:
205
210
  - lib/workos/webhook.rb
206
211
  - lib/workos/webhooks.rb
207
212
  - lib/workos/widgets.rb
213
+ - release-please-config.json
208
214
  - spec/lib/workos/audit_logs_spec.rb
209
215
  - spec/lib/workos/cache_spec.rb
210
216
  - spec/lib/workos/client.rb
211
217
  - spec/lib/workos/configuration_spec.rb
212
218
  - spec/lib/workos/directory_sync_spec.rb
213
219
  - spec/lib/workos/directory_user_spec.rb
220
+ - spec/lib/workos/encryptors/aes_gcm_spec.rb
214
221
  - spec/lib/workos/event_spec.rb
215
222
  - spec/lib/workos/mfa_spec.rb
216
223
  - spec/lib/workos/organizations_spec.rb
@@ -451,6 +458,7 @@ test_files:
451
458
  - spec/lib/workos/configuration_spec.rb
452
459
  - spec/lib/workos/directory_sync_spec.rb
453
460
  - spec/lib/workos/directory_user_spec.rb
461
+ - spec/lib/workos/encryptors/aes_gcm_spec.rb
454
462
  - spec/lib/workos/event_spec.rb
455
463
  - spec/lib/workos/mfa_spec.rb
456
464
  - spec/lib/workos/organizations_spec.rb
@@ -1,80 +0,0 @@
1
- name: Version Bump
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- bump_type:
7
- description: "Version bump type"
8
- required: true
9
- type: choice
10
- options:
11
- - patch
12
- - minor
13
- - major
14
-
15
- jobs:
16
- bump-version:
17
- runs-on: ubuntu-latest
18
- permissions:
19
- contents: write
20
- pull-requests: write
21
- steps:
22
- - name: Generate token
23
- id: generate-token
24
- uses: actions/create-github-app-token@v2
25
- with:
26
- app-id: ${{ vars.SDK_BOT_APP_ID }}
27
- private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
28
-
29
- - name: Checkout
30
- uses: actions/checkout@v6
31
- with:
32
- token: ${{ steps.generate-token.outputs.token }}
33
-
34
- - name: Configure Git
35
- run: |
36
- git config user.name "workos-bot[bot]"
37
- git config user.email "workos-bot[bot]@users.noreply.github.com"
38
-
39
- - name: Read current version
40
- id: current-version
41
- run: |
42
- CURRENT_VERSION=$(grep "VERSION = " lib/workos/version.rb | sed "s/.*VERSION = '\(.*\)'/\1/")
43
- echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
44
-
45
- - name: Bump version
46
- id: bump-version
47
- run: |
48
- CURRENT_VERSION="${{ steps.current-version.outputs.version }}"
49
- IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
50
-
51
- case "${{ github.event.inputs.bump_type }}" in
52
- major)
53
- NEW_VERSION="$((MAJOR + 1)).0.0"
54
- ;;
55
- minor)
56
- NEW_VERSION="$MAJOR.$((MINOR + 1)).0"
57
- ;;
58
- patch)
59
- NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
60
- ;;
61
- esac
62
-
63
- echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
64
-
65
- - name: Update version in version.rb
66
- run: |
67
- sed -i "s/VERSION = '.*'/VERSION = '${{ steps.bump-version.outputs.new_version }}'/" lib/workos/version.rb
68
-
69
- - name: Create Pull Request
70
- uses: peter-evans/create-pull-request@v7
71
- with:
72
- token: ${{ steps.generate-token.outputs.token }}
73
- commit-message: "v${{ steps.bump-version.outputs.new_version }}"
74
- title: "v${{ steps.bump-version.outputs.new_version }}"
75
- body: |
76
- Bumps version from ${{ steps.current-version.outputs.version }} to ${{ steps.bump-version.outputs.new_version }}.
77
-
78
- This PR was automatically created by the version-bump workflow.
79
- branch: version-bump-${{ steps.bump-version.outputs.new_version }}
80
- labels: version-bump