From 1e8e6a52cf31300a669d1714b1eb3346782565d2 Mon Sep 17 00:00:00 2001 From: Michael Hadley Date: Wed, 15 Jan 2025 13:15:47 -0800 Subject: [PATCH] Add `return_to` param to `UserManagement.get_logout_url` (#343) * Backfill test for `.get_logout_url` * Add `return_to` param to `UserManagement.get_logout_url` * Also update `Session#get_logout_url` --- lib/workos/session.rb | 7 ++--- lib/workos/user_management.rb | 8 +++-- spec/lib/workos/session_spec.rb | 42 ++++++++++++++++++------- spec/lib/workos/user_management_spec.rb | 21 +++++++++++++ 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/lib/workos/session.rb b/lib/workos/session.rb index 7a23b03b..11be446c 100644 --- a/lib/workos/session.rb +++ b/lib/workos/session.rb @@ -101,18 +101,17 @@ def refresh(options = nil) # rubocop:enable Metrics/PerceivedComplexity # Returns a URL to redirect the user to for logging out + # @param return_to [String] The URL to redirect the user to after logging out # @return [String] The URL to redirect the user to for logging out - # rubocop:disable Naming/AccessorMethodName - def get_logout_url + def get_logout_url(return_to: nil) 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]) + @user_management.get_logout_url(session_id: auth_response[:session_id], return_to: return_to) end - # rubocop:enable Naming/AccessorMethodName # Encrypts and seals data using AES-256-GCM # @param data [Hash] The data to seal diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb index ae6fcaca..62c2dbdd 100644 --- a/lib/workos/user_management.rb +++ b/lib/workos/user_management.rb @@ -530,13 +530,17 @@ def authenticate_with_email_verification( # # @param [String] session_id The session ID can be found in the `sid` # claim of the access token + # @param [String] return_to The URL to redirect the user to after logging out # # @return String - def get_logout_url(session_id:) + def get_logout_url(session_id:, return_to: nil) + params = { session_id: session_id } + params[:return_to] = return_to if return_to + URI::HTTPS.build( host: WorkOS.config.api_hostname, path: '/user_management/sessions/logout', - query: "session_id=#{session_id}", + query: URI.encode_www_form(params), ).to_s end diff --git a/spec/lib/workos/session_spec.rb b/spec/lib/workos/session_spec.rb index 1c0035a3..960f8b93 100644 --- a/spec/lib/workos/session_spec.rb +++ b/spec/lib/workos/session_spec.rb @@ -1,7 +1,6 @@ # 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' } @@ -10,11 +9,16 @@ 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 + let(:user_management) { instance_double('UserManagement') } + + before do + allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url) + end + it 'raises an error if cookie_password is nil or empty' do expect do WorkOS::Session.new( @@ -52,6 +56,7 @@ end describe '.authenticate' do + let(:user_management) { instance_double('UserManagement') } let(:valid_access_token) do payload = { sid: 'session_id', @@ -71,6 +76,10 @@ }, cookie_password,) end + before do + allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url) + end + it 'returns NO_SESSION_COOKIE_PROVIDED if session_data is nil' do session = WorkOS::Session.new( user_management: user_management, @@ -135,11 +144,13 @@ end describe '.refresh' do + let(:user_management) { instance_double('UserManagement') } 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(:get_jwks_url).with(client_id).and_return(jwks_url) allow(user_management).to receive(:authenticate_with_refresh_token).and_return(auth_response) end @@ -173,26 +184,33 @@ 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 + WorkOS::Session.new( + user_management: WorkOS::UserManagement, + 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', + session_id: 'session_123abc', 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') + expect(session.get_logout_url).to eq('https://api.workos.com/user_management/sessions/logout?session_id=session_123abc') + end + + context 'when given a return_to URL' do + it 'returns the logout URL with the return_to parameter' do + expect(session.get_logout_url(return_to: 'https://example.com/signed-out')).to eq( + 'https://api.workos.com/user_management/sessions/logout?session_id=session_123abc&return_to=https%3A%2F%2Fexample.com%2Fsigned-out', + ) + end end end diff --git a/spec/lib/workos/user_management_spec.rb b/spec/lib/workos/user_management_spec.rb index c26ab5ba..659b1a0c 100644 --- a/spec/lib/workos/user_management_spec.rb +++ b/spec/lib/workos/user_management_spec.rb @@ -1441,4 +1441,25 @@ end end end + + describe '.get_logout_url' do + it 'returns a logout url for the given session ID' do + result = described_class.get_logout_url( + session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6', + ) + + expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6' + end + + context 'when a `return_to` is given' do + it 'returns a logout url with the `return_to` query parameter' do + result = described_class.get_logout_url( + session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6', + return_to: 'https://example.com/signed-out', + ) + + expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6&return_to=https%3A%2F%2Fexample.com%2Fsigned-out' + end + end + end end