spec/acceptance/realtime/auth_spec.rb in ably-1.0.6 vs spec/acceptance/realtime/auth_spec.rb in ably-1.0.7

- old
+ new

@@ -1029,7 +1029,234 @@ expect(response).to be_a(Ably::Models::TokenDetails) stop_reactor end end end + + context 'when using JWT' do + let(:auth_url) { 'https://echo.ably.io/createJWT' } + let(:auth_params) { { keyName: key_name, keySecret: key_secret } } + let(:channel_name) { "test_JWT_#{random_str}" } + let(:message_name) { 'message_JWT' } + + # RSA8g + context 'when using auth_url' do + let(:client_options) { default_options.merge(auth_url: auth_url, auth_params: auth_params) } + + context 'when credentials are valid' do + it 'client successfully fetches a channel and publishes a message' do + channel = client.channels.get(channel_name) + channel.subscribe do |message| + expect(message.name).to eql(message_name) + stop_reactor + end + channel.publish message_name + end + end + + context 'when credentials are wrong' do + let(:auth_params) { { keyName: key_name, keySecret: 'invalid' } } + + it 'disconnected includes and invalid signature message' do + client.connection.once(:disconnected) do |state_change| + expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil + expect(state_change.reason.code).to eql(40144) + stop_reactor + end + client.connect + end + end + + context 'when token is expired' do + let(:token_duration) { 5 } + let(:auth_params) { { keyName: key_name, keySecret: key_secret, expiresIn: token_duration } } + it 'receives a 40142 error from the server' do + client.connection.once(:connected) do + client.connection.once(:disconnected) do |state_change| + expect(state_change.reason).to be_a(Ably::Models::ErrorInfo) + expect(state_change.reason.message).to match(/(expire)/i) + expect(state_change.reason.code).to eql(40142) + stop_reactor + end + end + end + end + end + + # RSA8g + context 'when using auth_callback' do + let(:token_callback) do + lambda do |token_params| + Ably::Rest::Client.new(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token + end + end + let(:client_options) { default_options.merge(auth_callback: token_callback) } + WebMock.allow_net_connect! + WebMock.disable! + context 'when credentials are valid' do + + it 'authentication succeeds and client can post a message' do + channel = client.channels.get(channel_name) + channel.subscribe do |message| + expect(message.name).to eql(message_name) + stop_reactor + end + channel.publish(message_name) do + # assert_requested :get, Addressable::Template.new("#{auth_url}{?keyName,keySecret}") + end + end + end + + context 'when credentials are invalid' do + let(:auth_params) { { keyName: key_name, keySecret: 'invalid' } } + + it 'authentication fails and reason for disconnection is invalid signature' do + client.connection.once(:disconnected) do |state_change| + expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil + expect(state_change.reason.code).to eql(40144) + stop_reactor + end + client.connect + end + end + end + + context 'when the client is initialized with ClientOptions and the token is a JWT token' do + let(:client_options) { { token: token, environment: environment, protocol: protocol } } + + context 'when credentials are valid' do + let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body } + + it 'posts successfully to a channel' do + channel = client.channels.get(channel_name) + channel.subscribe do |message| + expect(message.name).to eql(message_name) + stop_reactor + end + channel.publish(message_name) + end + end + + context 'when credentials are invalid' do + let(:key_secret) { 'invalid' } + let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body } + + it 'fails with an invalid signature error' do + client.connection.once(:disconnected) do |state_change| + expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil + expect(state_change.reason.code).to eql(40144) + stop_reactor + end + client.connect + end + end + end + + context 'when JWT token expires' do + before do + stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire + stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) # Ensure tokens issued expire immediately after issue + end + let(:token_callback) do + lambda do |token_params| + # Ably in all environments other than production will send AUTH 5 seconds before expiry, so + # we generate a JWT that expires in 5s so that the window for Realtime to send has passed + tokenResponse = Faraday.get "#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&expiresIn=5" + tokenResponse.body + end + end + let(:client_options) { default_options.merge(use_token_auth: true, auth_callback: token_callback) } + + # RTC8a + it 'client disconnects, a new token is requested via auth_callback and the client gets reconnected' do + client.connection.once(:connected) do + original_token = auth.current_token_details + original_conn_id = client.connection.id + + client.connection.once(:disconnected) do |state_change| + expect(state_change.reason.code).to eql(40142) + + client.connection.once(:connected) do + expect(original_token).to_not eql(auth.current_token_details) + expect(original_conn_id).to eql(client.connection.id) + stop_reactor + end + end + end + end + + context 'and an AUTH procol message is received' do + let(:token_callback) do + lambda do |token_params| + # Ably in all environments other than local will send AUTH 30 seconds before expiry + # We set the TTL to 35s so there's room to receive an AUTH protocol message + tokenResponse = Faraday.get "#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&expiresIn=35" + tokenResponse.body + end + end + + # RTC8a, RTC8a4 + it 'client reauths correctly without going through a disconnection' do + client.connection.once(:connected) do + original_token = client.auth.current_token_details + received_auth = false + + client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message| + received_auth = true if protocol_message.action == :auth + end + + client.connection.once(:update) do + expect(received_auth).to be_truthy + expect(original_token).to_not eql(client.auth.current_token_details) + stop_reactor + end + end + end + end + end + + context 'when the JWT token request includes a client_id' do + let(:client_id) { random_str } + let(:auth_callback) do + lambda do |token_params| + Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&client_id=#{client_id}").body + end + end + let(:client_options) { default_options.merge(auth_callback: auth_callback) } + + it 'the client_id is the same that was specified in the auth_callback that generated the JWT token' do + client.connection.once(:connected) do + expect(client.auth.client_id).to eql(client_id) + stop_reactor + end + end + end + + context 'when the JWT token request includes a subscribe-only capability' do + let(:channel_with_publish_permissions) { "test_JWT_with_publish_#{random_str}" } + let(:basic_capability) { JSON.dump(channel_name => ['subscribe'], channel_with_publish_permissions => ['publish']) } + let(:auth_callback) do + lambda do |token_params| + Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&capability=#{URI.escape(basic_capability)}").body + end + end + let(:client_options) { default_options.merge(auth_callback: auth_callback) } + + it 'client fails to publish to a channel with subscribe-only capability and publishes successfully on a channel with permissions' do + client.connection.once(:connected) do + forbidden_channel = client.channels.get(channel_name) + allowed_channel = client.channels.get(channel_with_publish_permissions) + forbidden_channel.publish('not-allowed').errback do |error| + expect(error.code).to eql(40160) + expect(error.message).to match(/permission denied/) + + allowed_channel.publish(message_name) do |message| + expect(message.name).to eql(message_name) + stop_reactor + end + end + end + end + end + end end end