From 778d5bca76d295356a0c4103e949d82f8028fc06 Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Fri, 23 Aug 2024 14:26:55 +0200
Subject: [PATCH 1/7] Session helpers

---
 .ruby-version                                 |   2 +-
 Gemfile.lock                                  |   2 +-
 lib/workos.rb                                 |   1 +
 lib/workos/authentication_response.rb         |  21 ++-
 lib/workos/refresh_authentication_response.rb |  25 ++-
 lib/workos/session.rb                         | 164 ++++++++++++++++++
 lib/workos/user_management.rb                 |  36 +++-
 7 files changed, 239 insertions(+), 12 deletions(-)
 create mode 100644 lib/workos/session.rb

diff --git a/.ruby-version b/.ruby-version
index b5021469..a0891f56 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.0.2
+3.3.4
diff --git a/Gemfile.lock b/Gemfile.lock
index 5c1da633..161130e2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -68,4 +68,4 @@ DEPENDENCIES
   workos!
 
 BUNDLED WITH
-   2.2.33
+   2.5.17
diff --git a/lib/workos.rb b/lib/workos.rb
index 03ecefc9..f492f4c2 100644
--- a/lib/workos.rb
+++ b/lib/workos.rb
@@ -71,6 +71,7 @@ def self.key
   autoload :Profile, 'workos/profile'
   autoload :ProfileAndToken, 'workos/profile_and_token'
   autoload :RefreshAuthenticationResponse, 'workos/refresh_authentication_response'
+  autoload :Session, 'workos/session'
   autoload :SSO, 'workos/sso'
   autoload :Types, 'workos/types'
   autoload :User, 'workos/user'
diff --git a/lib/workos/authentication_response.rb b/lib/workos/authentication_response.rb
index 3bba0040..feb5ca2a 100644
--- a/lib/workos/authentication_response.rb
+++ b/lib/workos/authentication_response.rb
@@ -6,10 +6,12 @@ module WorkOS
   class AuthenticationResponse
     include HashProvider
 
-    attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token
+    attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token, :authentication_method, :sealed_session
 
-    def initialize(authentication_response_json)
+    def initialize(authentication_response_json, session = nil)
       json = JSON.parse(authentication_response_json, symbolize_names: true)
+      @access_token = json[:access_token]
+      @refresh_token = json[:refresh_token]
       @user = WorkOS::User.new(json[:user].to_json)
       @organization_id = json[:organization_id]
       @impersonator =
@@ -17,8 +19,17 @@ def initialize(authentication_response_json)
           Impersonator.new(email: impersonator_json[:email],
                            reason: impersonator_json[:reason],)
         end
-      @access_token = json[:access_token]
-      @refresh_token = json[:refresh_token]
+      @authentication_method = json[:authentication_method]
+      @sealed_session =
+        if (session[:seal_session])
+          WorkOS::Session.seal_data({
+            access_token: access_token,
+            refresh_token: refresh_token,
+            user: user.to_json,
+            organization_id: organization_id,
+            impersonator: impersonator.to_json,
+          }, session[:cookie_password])
+        end
     end
 
     def to_json(*)
@@ -28,6 +39,8 @@ def to_json(*)
         impersonator: impersonator.to_json,
         access_token: access_token,
         refresh_token: refresh_token,
+        authentication_method: authentication_method,
+        sealed_session: sealed_session,
       }
     end
   end
diff --git a/lib/workos/refresh_authentication_response.rb b/lib/workos/refresh_authentication_response.rb
index 4885e404..0c20c61c 100644
--- a/lib/workos/refresh_authentication_response.rb
+++ b/lib/workos/refresh_authentication_response.rb
@@ -6,18 +6,39 @@ module WorkOS
   class RefreshAuthenticationResponse
     include HashProvider
 
-    attr_accessor :access_token, :refresh_token
+    attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token, :sealed_session
 
-    def initialize(authentication_response_json)
+    def initialize(authentication_response_json, session = nil)
       json = JSON.parse(authentication_response_json, symbolize_names: true)
       @access_token = json[:access_token]
       @refresh_token = json[:refresh_token]
+      @user = WorkOS::User.new(json[:user].to_json)
+      @organization_id = json[:organization_id]
+      @impersonator =
+        if (impersonator_json = json[:impersonator])
+          Impersonator.new(email: impersonator_json[:email],
+                           reason: impersonator_json[:reason],)
+        end
+      @sealed_session =
+        if (session[:seal_session])
+          WorkOS::Session.seal_data({
+            access_token: access_token,
+            refresh_token: refresh_token,
+            user: user.to_json,
+            organization_id: organization_id,
+            impersonator: impersonator.to_json,
+          }, session[:cookie_password])
+        end
     end
 
     def to_json(*)
       {
+        user: user.to_json,
+        organization_id: organization_id,
+        impersonator: impersonator.to_json,
         access_token: access_token,
         refresh_token: refresh_token,
+        sealed_session: sealed_session,
       }
     end
   end
diff --git a/lib/workos/session.rb b/lib/workos/session.rb
new file mode 100644
index 00000000..4d805a69
--- /dev/null
+++ b/lib/workos/session.rb
@@ -0,0 +1,164 @@
+require_relative './user_management'
+require 'jwt'
+require 'uri'
+require 'net/http'
+require 'encryptor'
+require 'securerandom'
+require 'json'
+require 'uri'
+
+module WorkOS
+  # The Session class provides helper methods for working with WorkOS sessions
+  # This class is not meant to be instantiated in a user space, and is instantiated internally but exposed.
+  class Session
+    attr_accessor :jwks, :jwks_algorithms, :user_management, :cookie_password, :session_data, :client_id
+
+    def initialize(user_management:, client_id:, session_data:, cookie_password:)
+      raise ArgumentError, 'cookiePassword is required' if cookie_password.nil? || cookie_password.empty?
+
+      @user_management = user_management
+      @cookie_password = cookie_password
+      @session_data = session_data
+      @client_id = client_id
+
+      @jwks = create_remote_jwk_set(URI(@user_management.get_jwks_url(client_id)))
+      @jwks_algorithms = @jwks.map { |key| key[:alg] }.compact.uniq
+    end
+
+    # Authenticates the user based on the session data
+    # @return [Hash] A hash containing the authentication response and a reason if the authentication failed
+    def authenticate
+      return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil?
+
+      begin
+        session = Session::unseal_data(@session_data, @cookie_password)
+      rescue StandardError => e
+        return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
+      end
+
+      return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:access_token]
+
+      return { authenticated: false, reason: 'INVALID_JWT' } unless self.is_valid_jwt(session[:access_token])
+
+      decoded = JWT.decode(session[:access_token], nil, true, algorithms: @jwks_algorithms, jwks: @jwks).first
+
+      {
+        authenticated: true,
+        session_id: decoded['sid'],
+        organization_id: decoded['org_id'],
+        role: decoded['role'],
+        permissions: decoded['permissions'],
+        user: session[:user],
+        impersonator: session[:impersonator],
+        reason: nil
+      }
+    end
+
+    # Refreshes the session data using the refresh token stored in the session data
+    # @param options [Hash] Options for refreshing the session
+    # @option options [String] :cookie_password The password to use for unsealing the session data
+    # @option options [String] :organization_id The organization ID to use for refreshing the session
+    # @return [Hash] A hash containing a new sealed session, the authentication response, and a reason if the refresh failed
+    def refresh(options = nil)
+      cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]
+
+      session = Session::unseal_data(@session_data, cookie_password)
+
+      return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:refresh_token] && session[:user]
+
+      begin
+        auth_response = @user_management.authenticate_with_refresh_token(
+          client_id: @client_id,
+          refresh_token: session[:refresh_token],
+          organization_id: options.nil? || options[:organization_id].nil? ? nil : options[:organization_id],
+          session: { :seal_session => true, :cookie_password => cookie_password }
+        )
+
+        @session_data = auth_response.sealed_session
+        @cookie_password = cookie_password
+
+        {
+          authenticated: true,
+          sealed_session: auth_response.sealed_session,
+          session: auth_response,
+          reason: nil
+        }
+      rescue StandardError => e
+        { authenticated: false, reason: e.message }
+      end
+    end
+
+    # Returns a URL to redirect the user to for logging out
+    # @return [String] The URL to redirect the user to for logging out
+    def get_logout_url
+      auth_response = authenticate
+
+      raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}" unless auth_response[:authenticated]
+
+      @user_management.get_logout_url(session_id: auth_response[:session_id])
+    end
+
+    private
+
+    # Creates a JWKS set from a remote JWKS URL
+    # @param uri [URI] The URI of the JWKS
+    # @return [JWT::JWK::Set] The JWKS set
+    def create_remote_jwk_set(uri)
+      # Fetch the JWKS from the remote URL
+      response = Net::HTTP.get(uri)
+
+      jwks_hash = JSON.parse(response)
+      jwks = JWT::JWK::Set.new(jwks_hash)
+
+      # filter jwks so it only returns the keys where 'use' is equal to 'sig'
+      jwks.keys.select! { |key| key[:use] == 'sig' }
+
+      jwks
+    end
+
+    # Validates a JWT token using the JWKS set
+    # @param token [String] The JWT token to validate
+    # @return [Boolean] True if the token is valid, false otherwise
+    def is_valid_jwt(token)
+      JWT.decode(token, nil, true, algorithms: @jwks_algorithms, jwks: @jwks)
+      true
+    rescue
+      false
+    end
+
+    # Encrypts and seals data using AES-256-GCM
+    # @param data [Hash] The data to seal
+    # @param key [String] The key to use for encryption
+    # @return [String] The sealed data
+    def self.seal_data(data, key)
+      iv = SecureRandom.random_bytes(12)
+
+      encrypted_data = Encryptor.encrypt(
+        value: JSON.generate(data),
+        key: key,
+        iv: iv,
+        algorithm: 'aes-256-gcm'
+      )
+      Base64.encode64(iv + encrypted_data) # Combine IV with encrypted data and encode as base64
+    end
+
+    # Decrypts and unseals data using AES-256-GCM
+    # @param sealed_data [String] The sealed data to unseal
+    # @param key [String] The key to use for decryption
+    # @return [Hash] The unsealed data
+    def self.unseal_data(sealed_data, key)
+      decoded_data = Base64.decode64(sealed_data)
+      iv = decoded_data[0..11] # Extract the IV (first 12 bytes)
+      encrypted_data = decoded_data[12..-1] # Extract the encrypted data
+
+      decrypted_data = Encryptor.decrypt(
+        value: encrypted_data,
+        key: key,
+        iv: iv,
+        algorithm: 'aes-256-gcm'
+      )
+
+      JSON.parse(decrypted_data, symbolize_names: true) # Parse the decrypted JSON string back to original data
+    end
+  end
+end
\ No newline at end of file
diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb
index 361c239c..f9fd4de1 100644
--- a/lib/workos/user_management.rb
+++ b/lib/workos/user_management.rb
@@ -37,6 +37,22 @@ class << self
       PROVIDERS = WorkOS::UserManagement::Types::Provider::ALL
       AUTH_FACTOR_TYPES = WorkOS::UserManagement::Types::AuthFactorType::ALL
 
+      # Load a sealed session
+      #
+      # @param [String] client_id The WorkOS client ID for the environment
+      # @param [String] session_data The sealed session data
+      # @param [String] cookie_password The password used to seal the session
+      #
+      # @return WorkOS::Session
+      def load_sealed_session(client_id:, session_data:, cookie_password:)
+        WorkOS::Session.new(
+          user_management: self,
+          client_id: client_id,
+          session_data: session_data,
+          cookie_password: cookie_password
+        )
+      end
+
       # Generate an OAuth 2.0 authorization URL that automatically directs a user
       # to their Identity Provider.
       #
@@ -289,14 +305,20 @@ def authenticate_with_password(email:, password:, client_id:, ip_address: nil, u
       # @param [String] client_id The WorkOS client ID for the environment
       # @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
       # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
+      # @param [Hash] session An optional hash that determines whether the session should be sealed and the cookie password.
       #
       # @return WorkOS::AuthenticationResponse
       def authenticate_with_code(
         code:,
         client_id:,
         ip_address: nil,
-        user_agent: nil
+        user_agent: nil,
+        session: nil
       )
+        if session[:seal_session] == true and session[:cookie_password].nil?
+          raise ArgumentError, 'cookie_password is required when sealing session'
+        end
+
         response = execute_request(
           request: post_request(
             path: '/user_management/authenticate',
@@ -311,7 +333,7 @@ def authenticate_with_code(
           ),
         )
 
-        WorkOS::AuthenticationResponse.new(response.body)
+        WorkOS::AuthenticationResponse.new(response.body, session)
       end
 
       # Authenticate a user using a refresh token.
@@ -321,6 +343,7 @@ def authenticate_with_code(
       # @param [String] organization_id The organization to issue the new access token for. (Optional)
       # @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
       # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
+      # @param [Hash] session An optional hash that determines whether the session should be sealed and the cookie password.
       #
       # @return WorkOS::RefreshAuthenticationResponse
       def authenticate_with_refresh_token(
@@ -328,8 +351,13 @@ def authenticate_with_refresh_token(
         client_id:,
         organization_id: nil,
         ip_address: nil,
-        user_agent: nil
+        user_agent: nil,
+        session: nil
       )
+        if session[:seal_session] == true and session[:cookie_password].nil?
+          raise ArgumentError, 'cookie_password is required when sealing session'
+        end
+
         response = execute_request(
           request: post_request(
             path: '/user_management/authenticate',
@@ -345,7 +373,7 @@ def authenticate_with_refresh_token(
           ),
         )
 
-        WorkOS::RefreshAuthenticationResponse.new(response.body)
+        WorkOS::RefreshAuthenticationResponse.new(response.body, session)
       end
 
       # Authenticate user by Magic Auth Code.

From a066b1845907f2da3327c85089b0a1106cee3cfd Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Tue, 27 Aug 2024 14:38:14 +0200
Subject: [PATCH 2/7] Add tests

---
 Gemfile                                       |   2 +
 Gemfile.lock                                  |   6 +
 lib/workos/authentication_response.rb         |   2 +-
 lib/workos/refresh_authentication_response.rb |   2 +-
 lib/workos/session.rb                         |   7 +-
 lib/workos/user_management.rb                 |   4 +-
 spec/lib/workos/session_spec.rb               | 144 ++++++++++++++++
 spec/lib/workos/user_management_spec.rb       |   1 +
 .../authenticate_with_refresh_token/valid.yml | 157 +++++++++---------
 9 files changed, 241 insertions(+), 84 deletions(-)
 create mode 100644 spec/lib/workos/session_spec.rb

diff --git a/Gemfile b/Gemfile
index 7f4f5e95..6075e3ea 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,3 +3,5 @@
 source 'https://rubygems.org'
 
 gemspec
+gem 'jwt'
+gem 'encryptor'
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
index 161130e2..326b64e5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -9,12 +9,16 @@ GEM
     addressable (2.8.6)
       public_suffix (>= 2.0.2, < 6.0)
     ast (2.4.2)
+    base64 (0.2.0)
     bigdecimal (3.1.7)
     crack (1.0.0)
       bigdecimal
       rexml
     diff-lcs (1.5.1)
+    encryptor (3.0.0)
     hashdiff (1.1.0)
+    jwt (2.8.2)
+      base64
     parallel (1.24.0)
     parser (3.3.0.5)
       ast (~> 2.4.1)
@@ -61,6 +65,8 @@ PLATFORMS
 
 DEPENDENCIES
   bundler (>= 2.0.1)
+  encryptor
+  jwt
   rspec (~> 3.9.0)
   rubocop (~> 0.77)
   vcr (~> 5.0.0)
diff --git a/lib/workos/authentication_response.rb b/lib/workos/authentication_response.rb
index feb5ca2a..eb19d9a6 100644
--- a/lib/workos/authentication_response.rb
+++ b/lib/workos/authentication_response.rb
@@ -21,7 +21,7 @@ def initialize(authentication_response_json, session = nil)
         end
       @authentication_method = json[:authentication_method]
       @sealed_session =
-        if (session[:seal_session])
+        if session and session[:seal_session]
           WorkOS::Session.seal_data({
             access_token: access_token,
             refresh_token: refresh_token,
diff --git a/lib/workos/refresh_authentication_response.rb b/lib/workos/refresh_authentication_response.rb
index 0c20c61c..e7f9d6c6 100644
--- a/lib/workos/refresh_authentication_response.rb
+++ b/lib/workos/refresh_authentication_response.rb
@@ -20,7 +20,7 @@ def initialize(authentication_response_json, session = nil)
                            reason: impersonator_json[:reason],)
         end
       @sealed_session =
-        if (session[:seal_session])
+        if session and session[:seal_session]
           WorkOS::Session.seal_data({
             access_token: access_token,
             refresh_token: refresh_token,
diff --git a/lib/workos/session.rb b/lib/workos/session.rb
index 4d805a69..210c0c30 100644
--- a/lib/workos/session.rb
+++ b/lib/workos/session.rb
@@ -1,4 +1,3 @@
-require_relative './user_management'
 require 'jwt'
 require 'uri'
 require 'net/http'
@@ -62,7 +61,11 @@ def authenticate
     def refresh(options = nil)
       cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]
 
-      session = Session::unseal_data(@session_data, cookie_password)
+      begin
+        session = Session::unseal_data(@session_data, cookie_password)
+      rescue StandardError => e
+        return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
+      end
 
       return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:refresh_token] && session[:user]
 
diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb
index f9fd4de1..0b73ea45 100644
--- a/lib/workos/user_management.rb
+++ b/lib/workos/user_management.rb
@@ -315,7 +315,7 @@ def authenticate_with_code(
         user_agent: nil,
         session: nil
       )
-        if session[:seal_session] == true and session[:cookie_password].nil?
+        if session and session[:seal_session] == true and session[:cookie_password].nil?
           raise ArgumentError, 'cookie_password is required when sealing session'
         end
 
@@ -354,7 +354,7 @@ def authenticate_with_refresh_token(
         user_agent: nil,
         session: nil
       )
-        if session[:seal_session] == true and session[:cookie_password].nil?
+        if session and session[:seal_session] == true and session[:cookie_password].nil?
           raise ArgumentError, 'cookie_password is required when sealing session'
         end
 
diff --git a/spec/lib/workos/session_spec.rb b/spec/lib/workos/session_spec.rb
new file mode 100644
index 00000000..e313848d
--- /dev/null
+++ b/spec/lib/workos/session_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+describe WorkOS::Session do
+
+  let(:user_management) { instance_double('UserManagement') }
+  let(:client_id) { 'test_client_id' }
+  let(:cookie_password) { 'test_very_long_cookie_password__' }
+  let(:session_data) { 'test_session_data' }
+  let(:jwks_url) { 'https://api.workos.com/sso/jwks/client_123' }
+  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"}]}' }
+  let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), { kid: 'sso_oidc_key_pair_123', use: 'sig', alg: 'RS256' }) }
+
+  before do
+    allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
+    allow(Net::HTTP).to receive(:get).and_return(jwks_hash)
+  end
+
+  describe 'initialize' do
+    it 'raises an error if cookie_password is nil or empty' do
+      expect {
+        WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: nil)
+      }.to raise_error(ArgumentError, 'cookiePassword is required')
+
+      expect {
+        WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: '')
+      }.to raise_error(ArgumentError, 'cookiePassword is required')
+    end
+
+    it 'initializes with valid parameters' do
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password)
+      expect(session.user_management).to eq(user_management)
+      expect(session.client_id).to eq(client_id)
+      expect(session.session_data).to eq(session_data)
+      expect(session.cookie_password).to eq(cookie_password)
+      expect(session.jwks.map(&:export)).to eq(JSON.parse(jwks_hash, symbolize_names: true)[:keys])
+      expect(session.jwks_algorithms).to eq(['RS256'])
+    end
+  end
+
+  describe '.authenticate' do
+    let(:valid_access_token) do
+      payload = { sid: 'session_id', org_id: 'org_id', role: 'role', permissions: ['read'], exp: Time.now.to_i + 3600 }
+      headers = { kid: jwk[:kid] }
+      JWT.encode(payload, jwk.signing_key, jwk[:alg], headers)
+    end
+    let(:session_data) { WorkOS::Session.seal_data({ access_token: valid_access_token, user: 'user', impersonator: 'impersonator' }, cookie_password) }
+
+    it 'returns NO_SESSION_COOKIE_PROVIDED if session_data is nil' do
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: nil, cookie_password: cookie_password)
+      result = session.authenticate
+      expect(result).to eq({ authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' })
+    end
+
+    it 'returns INVALID_SESSION_COOKIE if session_data is invalid' do
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: 'invalid_data', cookie_password: cookie_password)
+      result = session.authenticate
+      expect(result).to eq({ authenticated: false, reason: 'INVALID_SESSION_COOKIE' })
+    end
+
+    it 'returns INVALID_JWT if access_token is invalid' do
+      invalid_session_data = WorkOS::Session.seal_data({ access_token: 'invalid_token' }, cookie_password)
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: invalid_session_data, cookie_password: cookie_password)
+      result = session.authenticate
+      expect(result).to eq({ authenticated: false, reason: 'INVALID_JWT' })
+    end
+
+    it 'authenticates successfully with valid session_data' do
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password)
+      allow(session).to receive(:is_valid_jwt).and_return(true)
+      allow(JWT).to receive(:decode).and_return([{ 'sid' => 'session_id', 'org_id' => 'org_id', 'role' => 'role', 'permissions' => ['read'] }])
+
+      result = session.authenticate
+      expect(result).to eq({
+        authenticated: true,
+        session_id: 'session_id',
+        organization_id: 'org_id',
+        role: 'role',
+        permissions: ['read'],
+        user: 'user',
+        impersonator: 'impersonator',
+        reason: nil
+      })
+    end
+  end
+
+  describe '.refresh' do
+    let(:refresh_token) { 'test_refresh_token' }
+    let(:session_data) { WorkOS::Session.seal_data({ refresh_token: refresh_token, user: 'user' }, cookie_password) }
+    let(:auth_response) { double('AuthResponse', sealed_session: 'new_sealed_session') }
+
+    before do
+      allow(user_management).to receive(:authenticate_with_refresh_token).and_return(auth_response)
+    end
+
+    it 'returns INVALID_SESSION_COOKIE if session_data is invalid' do
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: 'invalid_data', cookie_password: cookie_password)
+      result = session.refresh
+      expect(result).to eq({ authenticated: false, reason: 'INVALID_SESSION_COOKIE' })
+    end
+
+    it 'refreshes the session successfully with valid session_data' do
+      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password)
+      result = session.refresh
+      expect(result).to eq({
+        authenticated: true,
+        sealed_session: 'new_sealed_session',
+        session: auth_response,
+        reason: nil
+      })
+    end
+  end
+
+  describe '.get_logout_url' do
+    let(:session) { WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password) }
+
+    context 'when authentication is successful' do
+      before do
+        allow(session).to receive(:authenticate).and_return({
+          authenticated: true,
+          session_id: 'session_id',
+          reason: nil
+        })
+        allow(user_management).to receive(:get_logout_url).with(session_id: 'session_id').and_return('https://example.com/logout')
+      end
+
+      it 'returns the logout URL' do
+        expect(session.get_logout_url).to eq('https://example.com/logout')
+      end
+    end
+
+    context 'when authentication fails' do
+      before do
+        allow(session).to receive(:authenticate).and_return({
+          authenticated: false,
+          reason: 'Invalid session'
+        })
+      end
+
+      it 'raises an error' do
+        expect { session.get_logout_url }.to raise_error(RuntimeError, 'Failed to extract session ID for logout URL: Invalid session')
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/lib/workos/user_management_spec.rb b/spec/lib/workos/user_management_spec.rb
index bea5b56a..a7723cbb 100644
--- a/spec/lib/workos/user_management_spec.rb
+++ b/spec/lib/workos/user_management_spec.rb
@@ -467,6 +467,7 @@
           )
           expect(authentication_response.access_token).to eq('<ACCESS_TOKEN>')
           expect(authentication_response.refresh_token).to eq('<REFRESH_TOKEN>')
+          expect(authentication_response.user.id).to eq('user_01H93WD0R0KWF8Q7BK02C0RPYJ')
         end
       end
     end
diff --git a/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml b/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml
index e8381d6a..dd1b3328 100644
--- a/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml
+++ b/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml
@@ -1,81 +1,82 @@
 ---
 http_interactions:
-- request:
-    method: post
-    uri: https://api.workos.com/user_management/authenticate
-    body:
-      encoding: UTF-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
-        (Macintosh; Intel Mac OS X 10_15_7) Chrome/108.0.0.0 Safari/537.36","grant_type":"refresh_token"}'
-    headers:
-      Content-Type:
-      - application/json
-      Accept-Encoding:
-      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
-      Accept:
-      - "*/*"
-      User-Agent:
-      - WorkOS; ruby/3.2.2; arm64-darwin22; v4.0.0
-  response:
-    status:
-      code: 200
-      message: OK
-    headers:
-      Date:
-      - Mon, 18 Mar 2024 19:00:53 GMT
-      Content-Type:
-      - application/json; charset=utf-8
-      Transfer-Encoding:
-      - chunked
-      Connection:
-      - keep-alive
-      Cf-Ray:
-      - 866777d63b4627e8-SLC
-      Cf-Cache-Status:
-      - DYNAMIC
-      Etag:
-      - W/"335-M3MDQYhs5724SayBHHCwnBDn3qA"
-      Strict-Transport-Security:
-      - max-age=15552000; includeSubDomains
-      Vary:
-      - Origin, Accept-Encoding
-      Via:
-      - 1.1 spaces-router (devel)
-      Access-Control-Allow-Credentials:
-      - 'true'
-      Content-Security-Policy:
-      - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
-        https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
-        ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
-      Expect-Ct:
-      - max-age=0
-      Referrer-Policy:
-      - no-referrer
-      X-Content-Type-Options:
-      - nosniff
-      X-Dns-Prefetch-Control:
-      - 'off'
-      X-Download-Options:
-      - noopen
-      X-Frame-Options:
-      - SAMEORIGIN
-      X-Permitted-Cross-Domain-Policies:
-      - none
-      X-Request-Id:
-      - 995ed1ed-e892-4049-86c9-0e07baa6cc4b
-      X-Xss-Protection:
-      - '0'
-      Set-Cookie:
-      - __cf_bm=2NHqv1cd1BisOc8KKcQ0oNzFxZZT4OHQd6c2QDuGnUU-1710788453-1.0.1.1-4BxBRzVrhL7rCH895PcfORXr_6Rnj3Oh5w1YG4xi7X1st62LMzb5dHZO7u7P.V1P8nBDAAt3Wbz7xsDTWrfWJg;
-        path=/; expires=Mon, 18-Mar-24 19:30:53 GMT; domain=.workos.com; HttpOnly;
-        Secure; SameSite=None
-      - __cfruid=06035c17e9b60a1d7a42a5b568146a0bb71a06dc-1710788453; path=/; domain=.workos.com;
-        HttpOnly; Secure; SameSite=None
-      Server:
-      - cloudflare
-    body:
-      encoding: UTF-8
-      string: '{"access_token":"<ACCESS_TOKEN>","refresh_token":"<REFRESH_TOKEN>"}'
-    http_version:
-  recorded_at: Mon, 18 Mar 2024 19:00:53 GMT
+  - request:
+      method: post
+      uri: https://api.workos.com/user_management/authenticate
+      body:
+        encoding: UTF-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
+          (Macintosh; Intel Mac OS X 10_15_7) Chrome/108.0.0.0 Safari/537.36","grant_type":"refresh_token"}'
+      headers:
+        Content-Type:
+          - application/json
+        Accept-Encoding:
+          - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+        Accept:
+          - '*/*'
+        User-Agent:
+          - WorkOS; ruby/3.2.2; arm64-darwin22; v4.0.0
+    response:
+      status:
+        code: 200
+        message: OK
+      headers:
+        Date:
+          - Mon, 18 Mar 2024 19:00:53 GMT
+        Content-Type:
+          - application/json; charset=utf-8
+        Transfer-Encoding:
+          - chunked
+        Connection:
+          - keep-alive
+        Cf-Ray:
+          - 866777d63b4627e8-SLC
+        Cf-Cache-Status:
+          - DYNAMIC
+        Etag:
+          - W/"335-M3MDQYhs5724SayBHHCwnBDn3qA"
+        Strict-Transport-Security:
+          - max-age=15552000; includeSubDomains
+        Vary:
+          - Origin, Accept-Encoding
+        Via:
+          - 1.1 spaces-router (devel)
+        Access-Control-Allow-Credentials:
+          - 'true'
+        Content-Security-Policy:
+          - "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self'
+            https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src
+            'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
+        Expect-Ct:
+          - max-age=0
+        Referrer-Policy:
+          - no-referrer
+        X-Content-Type-Options:
+          - nosniff
+        X-Dns-Prefetch-Control:
+          - 'off'
+        X-Download-Options:
+          - noopen
+        X-Frame-Options:
+          - SAMEORIGIN
+        X-Permitted-Cross-Domain-Policies:
+          - none
+        X-Request-Id:
+          - 995ed1ed-e892-4049-86c9-0e07baa6cc4b
+        X-Xss-Protection:
+          - '0'
+        Set-Cookie:
+          - __cf_bm=2NHqv1cd1BisOc8KKcQ0oNzFxZZT4OHQd6c2QDuGnUU-1710788453-1.0.1.1-4BxBRzVrhL7rCH895PcfORXr_6Rnj3Oh5w1YG4xi7X1st62LMzb5dHZO7u7P.V1P8nBDAAt3Wbz7xsDTWrfWJg;
+            path=/; expires=Mon, 18-Mar-24 19:30:53 GMT; domain=.workos.com; HttpOnly;
+            Secure; SameSite=None
+          - __cfruid=06035c17e9b60a1d7a42a5b568146a0bb71a06dc-1710788453; path=/; domain=.workos.com;
+            HttpOnly; Secure; SameSite=None
+        Server:
+          - cloudflare
+      body:
+        encoding: UTF-8
+        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>"}'
+      http_version:
+    recorded_at: Mon, 18 Mar 2024 19:00:53 GMT
 recorded_with: VCR 5.0.0

From 6c2de0c6404645b7165b3c56c269985a0b649689 Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Tue, 27 Aug 2024 15:05:07 +0200
Subject: [PATCH 3/7] Rubocop

---
 Gemfile                                       |   2 +-
 lib/workos/authentication_response.rb         |  24 ++-
 lib/workos/refresh_authentication_response.rb |  16 +-
 lib/workos/session.rb                         | 100 ++++++-----
 lib/workos/user_management.rb                 |  12 +-
 spec/lib/workos/session_spec.rb               | 156 +++++++++++++-----
 6 files changed, 204 insertions(+), 106 deletions(-)

diff --git a/Gemfile b/Gemfile
index 6075e3ea..cb47b448 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,5 +3,5 @@
 source 'https://rubygems.org'
 
 gemspec
+gem 'encryptor'
 gem 'jwt'
-gem 'encryptor'
\ No newline at end of file
diff --git a/lib/workos/authentication_response.rb b/lib/workos/authentication_response.rb
index eb19d9a6..84642790 100644
--- a/lib/workos/authentication_response.rb
+++ b/lib/workos/authentication_response.rb
@@ -6,8 +6,15 @@ module WorkOS
   class AuthenticationResponse
     include HashProvider
 
-    attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token, :authentication_method, :sealed_session
+    attr_accessor :user,
+                  :organization_id,
+                  :impersonator,
+                  :access_token,
+                  :refresh_token,
+                  :authentication_method,
+                  :sealed_session
 
+    # rubocop:disable Metrics/AbcSize
     def initialize(authentication_response_json, session = nil)
       json = JSON.parse(authentication_response_json, symbolize_names: true)
       @access_token = json[:access_token]
@@ -21,16 +28,17 @@ def initialize(authentication_response_json, session = nil)
         end
       @authentication_method = json[:authentication_method]
       @sealed_session =
-        if session and session[:seal_session]
+        if session && session[:seal_session]
           WorkOS::Session.seal_data({
-            access_token: access_token,
-            refresh_token: refresh_token,
-            user: user.to_json,
-            organization_id: organization_id,
-            impersonator: impersonator.to_json,
-          }, session[:cookie_password])
+                                      access_token: access_token,
+                                      refresh_token: refresh_token,
+                                      user: user.to_json,
+                                      organization_id: organization_id,
+                                      impersonator: impersonator.to_json,
+                                    }, session[:cookie_password],)
         end
     end
+    # rubocop:enable Metrics/AbcSize
 
     def to_json(*)
       {
diff --git a/lib/workos/refresh_authentication_response.rb b/lib/workos/refresh_authentication_response.rb
index e7f9d6c6..76d7ce73 100644
--- a/lib/workos/refresh_authentication_response.rb
+++ b/lib/workos/refresh_authentication_response.rb
@@ -8,6 +8,7 @@ class RefreshAuthenticationResponse
 
     attr_accessor :user, :organization_id, :impersonator, :access_token, :refresh_token, :sealed_session
 
+    # rubocop:disable Metrics/AbcSize
     def initialize(authentication_response_json, session = nil)
       json = JSON.parse(authentication_response_json, symbolize_names: true)
       @access_token = json[:access_token]
@@ -20,16 +21,17 @@ def initialize(authentication_response_json, session = nil)
                            reason: impersonator_json[:reason],)
         end
       @sealed_session =
-        if session and session[:seal_session]
+        if session && session[:seal_session]
           WorkOS::Session.seal_data({
-            access_token: access_token,
-            refresh_token: refresh_token,
-            user: user.to_json,
-            organization_id: organization_id,
-            impersonator: impersonator.to_json,
-          }, session[:cookie_password])
+                                      access_token: access_token,
+                                      refresh_token: refresh_token,
+                                      user: user.to_json,
+                                      organization_id: organization_id,
+                                      impersonator: impersonator.to_json,
+                                    }, session[:cookie_password],)
         end
     end
+    # rubocop:enable Metrics/AbcSize
 
     def to_json(*)
       {
diff --git a/lib/workos/session.rb b/lib/workos/session.rb
index 210c0c30..7a23b03b 100644
--- a/lib/workos/session.rb
+++ b/lib/workos/session.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'jwt'
 require 'uri'
 require 'net/http'
@@ -9,6 +11,7 @@
 module WorkOS
   # The Session class provides helper methods for working with WorkOS sessions
   # This class is not meant to be instantiated in a user space, and is instantiated internally but exposed.
+  # rubocop:disable Metrics/ClassLength
   class Session
     attr_accessor :jwks, :jwks_algorithms, :user_management, :cookie_password, :session_data, :client_id
 
@@ -30,14 +33,13 @@ def authenticate
       return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil?
 
       begin
-        session = Session::unseal_data(@session_data, @cookie_password)
-      rescue StandardError => e
+        session = Session.unseal_data(@session_data, @cookie_password)
+      rescue StandardError
         return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
       end
 
       return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:access_token]
-
-      return { authenticated: false, reason: 'INVALID_JWT' } unless self.is_valid_jwt(session[:access_token])
+      return { authenticated: false, reason: 'INVALID_JWT' } unless is_valid_jwt(session[:access_token])
 
       decoded = JWT.decode(session[:access_token], nil, true, algorithms: @jwks_algorithms, jwks: @jwks).first
 
@@ -49,7 +51,7 @@ def authenticate
         permissions: decoded['permissions'],
         user: session[:user],
         impersonator: session[:impersonator],
-        reason: nil
+        reason: nil,
       }
     end
 
@@ -57,13 +59,17 @@ def authenticate
     # @param options [Hash] Options for refreshing the session
     # @option options [String] :cookie_password The password to use for unsealing the session data
     # @option options [String] :organization_id The organization ID to use for refreshing the session
-    # @return [Hash] A hash containing a new sealed session, the authentication response, and a reason if the refresh failed
+    # @return [Hash] A hash containing a new sealed session, the authentication response,
+    # and a reason if the refresh failed
+    # rubocop:disable Metrics/AbcSize
+    # rubocop:disable Metrics/CyclomaticComplexity
+    # rubocop:disable Metrics/PerceivedComplexity
     def refresh(options = nil)
       cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]
 
       begin
-        session = Session::unseal_data(@session_data, cookie_password)
-      rescue StandardError => e
+        session = Session.unseal_data(@session_data, cookie_password)
+      rescue StandardError
         return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
       end
 
@@ -74,7 +80,7 @@ def refresh(options = nil)
           client_id: @client_id,
           refresh_token: session[:refresh_token],
           organization_id: options.nil? || options[:organization_id].nil? ? nil : options[:organization_id],
-          session: { :seal_session => true, :cookie_password => cookie_password }
+          session: { seal_session: true, cookie_password: cookie_password },
         )
 
         @session_data = auth_response.sealed_session
@@ -84,50 +90,29 @@ def refresh(options = nil)
           authenticated: true,
           sealed_session: auth_response.sealed_session,
           session: auth_response,
-          reason: nil
+          reason: nil,
         }
       rescue StandardError => e
         { authenticated: false, reason: e.message }
       end
     end
+    # rubocop:enable Metrics/AbcSize
+    # rubocop:enable Metrics/CyclomaticComplexity
+    # rubocop:enable Metrics/PerceivedComplexity
 
     # Returns a URL to redirect the user to for logging out
     # @return [String] The URL to redirect the user to for logging out
+    # rubocop:disable Naming/AccessorMethodName
     def get_logout_url
       auth_response = authenticate
 
-      raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}" unless auth_response[:authenticated]
+      unless auth_response[:authenticated]
+        raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}"
+      end
 
       @user_management.get_logout_url(session_id: auth_response[:session_id])
     end
-
-    private
-
-    # Creates a JWKS set from a remote JWKS URL
-    # @param uri [URI] The URI of the JWKS
-    # @return [JWT::JWK::Set] The JWKS set
-    def create_remote_jwk_set(uri)
-      # Fetch the JWKS from the remote URL
-      response = Net::HTTP.get(uri)
-
-      jwks_hash = JSON.parse(response)
-      jwks = JWT::JWK::Set.new(jwks_hash)
-
-      # filter jwks so it only returns the keys where 'use' is equal to 'sig'
-      jwks.keys.select! { |key| key[:use] == 'sig' }
-
-      jwks
-    end
-
-    # Validates a JWT token using the JWKS set
-    # @param token [String] The JWT token to validate
-    # @return [Boolean] True if the token is valid, false otherwise
-    def is_valid_jwt(token)
-      JWT.decode(token, nil, true, algorithms: @jwks_algorithms, jwks: @jwks)
-      true
-    rescue
-      false
-    end
+    # rubocop:enable Naming/AccessorMethodName
 
     # Encrypts and seals data using AES-256-GCM
     # @param data [Hash] The data to seal
@@ -140,7 +125,7 @@ def self.seal_data(data, key)
         value: JSON.generate(data),
         key: key,
         iv: iv,
-        algorithm: 'aes-256-gcm'
+        algorithm: 'aes-256-gcm',
       )
       Base64.encode64(iv + encrypted_data) # Combine IV with encrypted data and encode as base64
     end
@@ -158,10 +143,41 @@ def self.unseal_data(sealed_data, key)
         value: encrypted_data,
         key: key,
         iv: iv,
-        algorithm: 'aes-256-gcm'
+        algorithm: 'aes-256-gcm',
       )
 
       JSON.parse(decrypted_data, symbolize_names: true) # Parse the decrypted JSON string back to original data
     end
+
+    private
+
+    # Creates a JWKS set from a remote JWKS URL
+    # @param uri [URI] The URI of the JWKS
+    # @return [JWT::JWK::Set] The JWKS set
+    def create_remote_jwk_set(uri)
+      # Fetch the JWKS from the remote URL
+      response = Net::HTTP.get(uri)
+
+      jwks_hash = JSON.parse(response)
+      jwks = JWT::JWK::Set.new(jwks_hash)
+
+      # filter jwks so it only returns the keys where 'use' is equal to 'sig'
+      jwks.keys.select! { |key| key[:use] == 'sig' }
+
+      jwks
+    end
+
+    # Validates a JWT token using the JWKS set
+    # @param token [String] The JWT token to validate
+    # @return [Boolean] True if the token is valid, false otherwise
+    # rubocop:disable Naming/PredicateName
+    def is_valid_jwt(token)
+      JWT.decode(token, nil, true, algorithms: @jwks_algorithms, jwks: @jwks)
+      true
+    rescue StandardError
+      false
+    end
+    # rubocop:enable Naming/PredicateName
   end
-end
\ No newline at end of file
+  # rubocop:enable Metrics/ClassLength
+end
diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb
index 0b73ea45..ae6fcaca 100644
--- a/lib/workos/user_management.rb
+++ b/lib/workos/user_management.rb
@@ -49,7 +49,7 @@ def load_sealed_session(client_id:, session_data:, cookie_password:)
           user_management: self,
           client_id: client_id,
           session_data: session_data,
-          cookie_password: cookie_password
+          cookie_password: cookie_password,
         )
       end
 
@@ -305,7 +305,8 @@ def authenticate_with_password(email:, password:, client_id:, ip_address: nil, u
       # @param [String] client_id The WorkOS client ID for the environment
       # @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
       # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
-      # @param [Hash] session An optional hash that determines whether the session should be sealed and the cookie password.
+      # @param [Hash] session An optional hash that determines whether the session should be sealed and
+      # the optional cookie password.
       #
       # @return WorkOS::AuthenticationResponse
       def authenticate_with_code(
@@ -315,7 +316,7 @@ def authenticate_with_code(
         user_agent: nil,
         session: nil
       )
-        if session and session[:seal_session] == true and session[:cookie_password].nil?
+        if session && (session[:seal_session] == true) && session[:cookie_password].nil?
           raise ArgumentError, 'cookie_password is required when sealing session'
         end
 
@@ -343,7 +344,8 @@ def authenticate_with_code(
       # @param [String] organization_id The organization to issue the new access token for. (Optional)
       # @param [String] ip_address The IP address of the request from the user who is attempting to authenticate.
       # @param [String] user_agent The user agent of the request from the user who is attempting to authenticate.
-      # @param [Hash] session An optional hash that determines whether the session should be sealed and the cookie password.
+      # @param [Hash] session An optional hash that determines whether the session should be sealed and
+      # the optional cookie password.
       #
       # @return WorkOS::RefreshAuthenticationResponse
       def authenticate_with_refresh_token(
@@ -354,7 +356,7 @@ def authenticate_with_refresh_token(
         user_agent: nil,
         session: nil
       )
-        if session and session[:seal_session] == true and session[:cookie_password].nil?
+        if session && (session[:seal_session] == true) && session[:cookie_password].nil?
           raise ArgumentError, 'cookie_password is required when sealing session'
         end
 
diff --git a/spec/lib/workos/session_spec.rb b/spec/lib/workos/session_spec.rb
index e313848d..1c0035a3 100644
--- a/spec/lib/workos/session_spec.rb
+++ b/spec/lib/workos/session_spec.rb
@@ -1,13 +1,12 @@
 # frozen_string_literal: true
 
 describe WorkOS::Session do
-
   let(:user_management) { instance_double('UserManagement') }
   let(:client_id) { 'test_client_id' }
   let(:cookie_password) { 'test_very_long_cookie_password__' }
   let(:session_data) { 'test_session_data' }
   let(:jwks_url) { 'https://api.workos.com/sso/jwks/client_123' }
-  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"}]}' }
+  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
   let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), { kid: 'sso_oidc_key_pair_123', use: 'sig', alg: 'RS256' }) }
 
   before do
@@ -17,17 +16,32 @@
 
   describe 'initialize' do
     it 'raises an error if cookie_password is nil or empty' do
-      expect {
-        WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: nil)
-      }.to raise_error(ArgumentError, 'cookiePassword is required')
-
-      expect {
-        WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: '')
-      }.to raise_error(ArgumentError, 'cookiePassword is required')
+      expect do
+        WorkOS::Session.new(
+          user_management: user_management,
+          client_id: client_id,
+          session_data: session_data,
+          cookie_password: nil,
+        )
+      end.to raise_error(ArgumentError, 'cookiePassword is required')
+
+      expect do
+        WorkOS::Session.new(
+          user_management: user_management,
+          client_id: client_id,
+          session_data: session_data,
+          cookie_password: '',
+        )
+      end.to raise_error(ArgumentError, 'cookiePassword is required')
     end
 
     it 'initializes with valid parameters' do
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: session_data,
+        cookie_password: cookie_password,
+      )
       expect(session.user_management).to eq(user_management)
       expect(session.client_id).to eq(client_id)
       expect(session.session_data).to eq(session_data)
@@ -39,47 +53,84 @@
 
   describe '.authenticate' do
     let(:valid_access_token) do
-      payload = { sid: 'session_id', org_id: 'org_id', role: 'role', permissions: ['read'], exp: Time.now.to_i + 3600 }
+      payload = {
+        sid: 'session_id',
+        org_id: 'org_id',
+        role: 'role',
+        permissions: ['read'],
+        exp: Time.now.to_i + 3600,
+      }
       headers = { kid: jwk[:kid] }
       JWT.encode(payload, jwk.signing_key, jwk[:alg], headers)
     end
-    let(:session_data) { WorkOS::Session.seal_data({ access_token: valid_access_token, user: 'user', impersonator: 'impersonator' }, cookie_password) }
+    let(:session_data) do
+  WorkOS::Session.seal_data({
+                              access_token: valid_access_token,
+                              user: 'user',
+                              impersonator: 'impersonator',
+                            }, cookie_password,)
+end
 
     it 'returns NO_SESSION_COOKIE_PROVIDED if session_data is nil' do
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: nil, cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: nil,
+        cookie_password: cookie_password,
+      )
       result = session.authenticate
       expect(result).to eq({ authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' })
     end
 
     it 'returns INVALID_SESSION_COOKIE if session_data is invalid' do
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: 'invalid_data', cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: 'invalid_data',
+        cookie_password: cookie_password,
+      )
       result = session.authenticate
       expect(result).to eq({ authenticated: false, reason: 'INVALID_SESSION_COOKIE' })
     end
 
     it 'returns INVALID_JWT if access_token is invalid' do
       invalid_session_data = WorkOS::Session.seal_data({ access_token: 'invalid_token' }, cookie_password)
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: invalid_session_data, cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: invalid_session_data,
+        cookie_password: cookie_password,
+      )
       result = session.authenticate
       expect(result).to eq({ authenticated: false, reason: 'INVALID_JWT' })
     end
 
     it 'authenticates successfully with valid session_data' do
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: session_data,
+        cookie_password: cookie_password,
+      )
       allow(session).to receive(:is_valid_jwt).and_return(true)
-      allow(JWT).to receive(:decode).and_return([{ 'sid' => 'session_id', 'org_id' => 'org_id', 'role' => 'role', 'permissions' => ['read'] }])
+      allow(JWT).to receive(:decode).and_return([{
+                                                  'sid' => 'session_id',
+                                                  'org_id' => 'org_id',
+                                                  'role' => 'role',
+                                                  'permissions' => ['read'],
+                                                }])
 
       result = session.authenticate
       expect(result).to eq({
-        authenticated: true,
-        session_id: 'session_id',
-        organization_id: 'org_id',
-        role: 'role',
-        permissions: ['read'],
-        user: 'user',
-        impersonator: 'impersonator',
-        reason: nil
-      })
+                             authenticated: true,
+                             session_id: 'session_id',
+                             organization_id: 'org_id',
+                             role: 'role',
+                             permissions: ['read'],
+                             user: 'user',
+                             impersonator: 'impersonator',
+                             reason: nil,
+                           })
     end
   end
 
@@ -93,33 +144,50 @@
     end
 
     it 'returns INVALID_SESSION_COOKIE if session_data is invalid' do
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: 'invalid_data', cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: 'invalid_data',
+        cookie_password: cookie_password,
+      )
       result = session.refresh
       expect(result).to eq({ authenticated: false, reason: 'INVALID_SESSION_COOKIE' })
     end
 
     it 'refreshes the session successfully with valid session_data' do
-      session = WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password)
+      session = WorkOS::Session.new(
+        user_management: user_management,
+        client_id: client_id,
+        session_data: session_data,
+        cookie_password: cookie_password,
+      )
       result = session.refresh
       expect(result).to eq({
-        authenticated: true,
-        sealed_session: 'new_sealed_session',
-        session: auth_response,
-        reason: nil
-      })
+                             authenticated: true,
+                             sealed_session: 'new_sealed_session',
+                             session: auth_response,
+                             reason: nil,
+                           })
     end
   end
 
   describe '.get_logout_url' do
-    let(:session) { WorkOS::Session.new(user_management: user_management, client_id: client_id, session_data: session_data, cookie_password: cookie_password) }
+    let(:session) do
+    WorkOS::Session.new(
+      user_management: user_management,
+      client_id: client_id,
+      session_data: session_data,
+      cookie_password: cookie_password,
+    )
+  end
 
     context 'when authentication is successful' do
       before do
         allow(session).to receive(:authenticate).and_return({
-          authenticated: true,
-          session_id: 'session_id',
-          reason: nil
-        })
+                                                              authenticated: true,
+                                                              session_id: 'session_id',
+                                                              reason: nil,
+                                                            })
         allow(user_management).to receive(:get_logout_url).with(session_id: 'session_id').and_return('https://example.com/logout')
       end
 
@@ -131,14 +199,16 @@
     context 'when authentication fails' do
       before do
         allow(session).to receive(:authenticate).and_return({
-          authenticated: false,
-          reason: 'Invalid session'
-        })
+                                                              authenticated: false,
+                                                              reason: 'Invalid session',
+                                                            })
       end
 
       it 'raises an error' do
-        expect { session.get_logout_url }.to raise_error(RuntimeError, 'Failed to extract session ID for logout URL: Invalid session')
+        expect { session.get_logout_url }.to raise_error(
+          RuntimeError, 'Failed to extract session ID for logout URL: Invalid session',
+        )
       end
     end
   end
-end
\ No newline at end of file
+end

From c3b7a76d5801bf52a47c5d9e6a71a624f0a42aa4 Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Tue, 27 Aug 2024 15:18:33 +0200
Subject: [PATCH 4/7] Put ruby back where it was

---
 .ruby-version | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.ruby-version b/.ruby-version
index a0891f56..b5021469 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.3.4
+3.0.2

From 2c87085cf826aef798519a5d071471916cfd90d6 Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Tue, 27 Aug 2024 15:20:10 +0200
Subject: [PATCH 5/7] And the gemfile

---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 326b64e5..304014cb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -74,4 +74,4 @@ DEPENDENCIES
   workos!
 
 BUNDLED WITH
-   2.5.17
+   2.2.33

From b84ccd9e106006d8c142a113e8d2ccfdc641cc0e Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Tue, 27 Aug 2024 16:35:54 +0200
Subject: [PATCH 6/7] move gems to gemspec

---
 Gemfile        | 2 --
 Gemfile.lock   | 4 ++--
 workos.gemspec | 3 +++
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/Gemfile b/Gemfile
index cb47b448..7f4f5e95 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,5 +3,3 @@
 source 'https://rubygems.org'
 
 gemspec
-gem 'encryptor'
-gem 'jwt'
diff --git a/Gemfile.lock b/Gemfile.lock
index 304014cb..559de6fc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,6 +2,8 @@ PATH
   remote: .
   specs:
     workos (5.6.0)
+      encryptor (~> 3.0)
+      jwt (~> 2.8)
 
 GEM
   remote: https://rubygems.org/
@@ -65,8 +67,6 @@ PLATFORMS
 
 DEPENDENCIES
   bundler (>= 2.0.1)
-  encryptor
-  jwt
   rspec (~> 3.9.0)
   rubocop (~> 0.77)
   vcr (~> 5.0.0)
diff --git a/workos.gemspec b/workos.gemspec
index 524c5a28..37636dae 100644
--- a/workos.gemspec
+++ b/workos.gemspec
@@ -21,6 +21,9 @@ Gem::Specification.new do |spec|
   spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
   spec.require_paths = ['lib']
 
+  spec.add_dependency 'jwt', '~> 2.8'
+  spec.add_dependency 'encryptor', '~> 3.0'
+
   spec.add_development_dependency 'bundler', '>= 2.0.1'
   spec.add_development_dependency 'rspec', '~> 3.9.0'
   spec.add_development_dependency 'rubocop', '~> 0.77'

From a77a08fb31f4d03f447fde25de6edec422b1a65b Mon Sep 17 00:00:00 2001
From: Paul Asjes <p.asjes@gmail.com>
Date: Tue, 27 Aug 2024 16:46:04 +0200
Subject: [PATCH 7/7] rubocop again

---
 workos.gemspec | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/workos.gemspec b/workos.gemspec
index 37636dae..98d092b9 100644
--- a/workos.gemspec
+++ b/workos.gemspec
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
   spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
   spec.require_paths = ['lib']
 
-  spec.add_dependency 'jwt', '~> 2.8'
   spec.add_dependency 'encryptor', '~> 3.0'
+  spec.add_dependency 'jwt', '~> 2.8'
 
   spec.add_development_dependency 'bundler', '>= 2.0.1'
   spec.add_development_dependency 'rspec', '~> 3.9.0'