diff --git a/Gemfile.lock b/Gemfile.lock index 5c1da633..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/ @@ -9,12 +11,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) 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..84642790 100644 --- a/lib/workos/authentication_response.rb +++ b/lib/workos/authentication_response.rb @@ -6,10 +6,19 @@ 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) + # 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] + @refresh_token = json[:refresh_token] @user = WorkOS::User.new(json[:user].to_json) @organization_id = json[:organization_id] @impersonator = @@ -17,9 +26,19 @@ 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 && 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 + # rubocop:enable Metrics/AbcSize def to_json(*) { @@ -28,6 +47,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..76d7ce73 100644 --- a/lib/workos/refresh_authentication_response.rb +++ b/lib/workos/refresh_authentication_response.rb @@ -6,18 +6,41 @@ 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) + # 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] @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 && 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 + # rubocop:enable Metrics/AbcSize 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..7a23b03b --- /dev/null +++ b/lib/workos/session.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +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. + # rubocop:disable Metrics/ClassLength + 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 + 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 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 + # 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 + return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } + end + + 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 + # 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 + + 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 + # rubocop:enable Naming/AccessorMethodName + + # 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 + + 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 + # rubocop:enable Metrics/ClassLength +end diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb index 361c239c..ae6fcaca 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,21 @@ 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 optional 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 && (session[:seal_session] == true) && 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 +334,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 +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 optional cookie password. # # @return WorkOS::RefreshAuthenticationResponse def authenticate_with_refresh_token( @@ -328,8 +353,13 @@ def authenticate_with_refresh_token( client_id:, organization_id: nil, ip_address: nil, - user_agent: nil + user_agent: nil, + session: nil ) + if session && (session[:seal_session] == true) && 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 +375,7 @@ def authenticate_with_refresh_token( ), ) - WorkOS::RefreshAuthenticationResponse.new(response.body) + WorkOS::RefreshAuthenticationResponse.new(response.body, session) end # Authenticate user by Magic Auth Code. diff --git a/spec/lib/workos/session_spec.rb b/spec/lib/workos/session_spec.rb new file mode 100644 index 00000000..1c0035a3 --- /dev/null +++ b/spec/lib/workos/session_spec.rb @@ -0,0 +1,214 @@ +# 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"}]}' } # 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 + 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 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, + ) + 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) 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, + ) + 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) 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, + }) + 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 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('') expect(authentication_response.refresh_token).to eq('') + 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":"","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":"","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":"","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":"","refresh_token":""}' + http_version: + recorded_at: Mon, 18 Mar 2024 19:00:53 GMT recorded_with: VCR 5.0.0 diff --git a/workos.gemspec b/workos.gemspec index 524c5a28..98d092b9 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 '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' spec.add_development_dependency 'rubocop', '~> 0.77'