ably 1.0.6 → 1.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -1
- data/README.md +3 -2
- data/SPEC.md +583 -496
- data/lib/ably/auth.rb +2 -2
- data/lib/ably/rest/client.rb +1 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/auth_spec.rb +227 -0
- data/spec/acceptance/rest/auth_spec.rb +37 -0
- metadata +2 -2
data/lib/ably/auth.rb
CHANGED
@@ -631,7 +631,7 @@ module Ably
|
|
631
631
|
uri = URI.parse(auth_url)
|
632
632
|
connection = Faraday.new("#{uri.scheme}://#{uri.host}", connection_options)
|
633
633
|
method = auth_options[:auth_method] || options[:auth_method] || :get
|
634
|
-
params = (auth_options[:auth_params] || options[:
|
634
|
+
params = (auth_options[:auth_params] || options[:auth_params] || {}).merge(token_params)
|
635
635
|
|
636
636
|
response = connection.public_send(method) do |request|
|
637
637
|
request.url uri.path
|
@@ -643,7 +643,7 @@ module Ably
|
|
643
643
|
end
|
644
644
|
end
|
645
645
|
|
646
|
-
if !response.body.kind_of?(Hash) && !response.headers['Content-Type'].to_s.match(%r{text/plain}i)
|
646
|
+
if !response.body.kind_of?(Hash) && !response.headers['Content-Type'].to_s.match(%r{text/plain|application/jwt}i)
|
647
647
|
raise Ably::Exceptions::InvalidResponseBody,
|
648
648
|
"Content Type #{response.headers['Content-Type']} is not supported by this client library"
|
649
649
|
end
|
data/lib/ably/rest/client.rb
CHANGED
@@ -117,6 +117,7 @@ module Ably
|
|
117
117
|
#
|
118
118
|
# @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used
|
119
119
|
# @option options [Array<String>] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled
|
120
|
+
# @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request.
|
120
121
|
#
|
121
122
|
# @return [Ably::Rest::Client]
|
122
123
|
#
|
data/lib/ably/version.rb
CHANGED
@@ -1031,5 +1031,232 @@ describe Ably::Realtime::Auth, :event_machine do
|
|
1031
1031
|
end
|
1032
1032
|
end
|
1033
1033
|
end
|
1034
|
+
|
1035
|
+
context 'when using JWT' do
|
1036
|
+
let(:auth_url) { 'https://echo.ably.io/createJWT' }
|
1037
|
+
let(:auth_params) { { keyName: key_name, keySecret: key_secret } }
|
1038
|
+
let(:channel_name) { "test_JWT_#{random_str}" }
|
1039
|
+
let(:message_name) { 'message_JWT' }
|
1040
|
+
|
1041
|
+
# RSA8g
|
1042
|
+
context 'when using auth_url' do
|
1043
|
+
let(:client_options) { default_options.merge(auth_url: auth_url, auth_params: auth_params) }
|
1044
|
+
|
1045
|
+
context 'when credentials are valid' do
|
1046
|
+
it 'client successfully fetches a channel and publishes a message' do
|
1047
|
+
channel = client.channels.get(channel_name)
|
1048
|
+
channel.subscribe do |message|
|
1049
|
+
expect(message.name).to eql(message_name)
|
1050
|
+
stop_reactor
|
1051
|
+
end
|
1052
|
+
channel.publish message_name
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
context 'when credentials are wrong' do
|
1057
|
+
let(:auth_params) { { keyName: key_name, keySecret: 'invalid' } }
|
1058
|
+
|
1059
|
+
it 'disconnected includes and invalid signature message' do
|
1060
|
+
client.connection.once(:disconnected) do |state_change|
|
1061
|
+
expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
|
1062
|
+
expect(state_change.reason.code).to eql(40144)
|
1063
|
+
stop_reactor
|
1064
|
+
end
|
1065
|
+
client.connect
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
context 'when token is expired' do
|
1070
|
+
let(:token_duration) { 5 }
|
1071
|
+
let(:auth_params) { { keyName: key_name, keySecret: key_secret, expiresIn: token_duration } }
|
1072
|
+
it 'receives a 40142 error from the server' do
|
1073
|
+
client.connection.once(:connected) do
|
1074
|
+
client.connection.once(:disconnected) do |state_change|
|
1075
|
+
expect(state_change.reason).to be_a(Ably::Models::ErrorInfo)
|
1076
|
+
expect(state_change.reason.message).to match(/(expire)/i)
|
1077
|
+
expect(state_change.reason.code).to eql(40142)
|
1078
|
+
stop_reactor
|
1079
|
+
end
|
1080
|
+
end
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
# RSA8g
|
1086
|
+
context 'when using auth_callback' do
|
1087
|
+
let(:token_callback) do
|
1088
|
+
lambda do |token_params|
|
1089
|
+
Ably::Rest::Client.new(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token
|
1090
|
+
end
|
1091
|
+
end
|
1092
|
+
let(:client_options) { default_options.merge(auth_callback: token_callback) }
|
1093
|
+
WebMock.allow_net_connect!
|
1094
|
+
WebMock.disable!
|
1095
|
+
context 'when credentials are valid' do
|
1096
|
+
|
1097
|
+
it 'authentication succeeds and client can post a message' do
|
1098
|
+
channel = client.channels.get(channel_name)
|
1099
|
+
channel.subscribe do |message|
|
1100
|
+
expect(message.name).to eql(message_name)
|
1101
|
+
stop_reactor
|
1102
|
+
end
|
1103
|
+
channel.publish(message_name) do
|
1104
|
+
# assert_requested :get, Addressable::Template.new("#{auth_url}{?keyName,keySecret}")
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
context 'when credentials are invalid' do
|
1110
|
+
let(:auth_params) { { keyName: key_name, keySecret: 'invalid' } }
|
1111
|
+
|
1112
|
+
it 'authentication fails and reason for disconnection is invalid signature' do
|
1113
|
+
client.connection.once(:disconnected) do |state_change|
|
1114
|
+
expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
|
1115
|
+
expect(state_change.reason.code).to eql(40144)
|
1116
|
+
stop_reactor
|
1117
|
+
end
|
1118
|
+
client.connect
|
1119
|
+
end
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
context 'when the client is initialized with ClientOptions and the token is a JWT token' do
|
1124
|
+
let(:client_options) { { token: token, environment: environment, protocol: protocol } }
|
1125
|
+
|
1126
|
+
context 'when credentials are valid' do
|
1127
|
+
let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body }
|
1128
|
+
|
1129
|
+
it 'posts successfully to a channel' do
|
1130
|
+
channel = client.channels.get(channel_name)
|
1131
|
+
channel.subscribe do |message|
|
1132
|
+
expect(message.name).to eql(message_name)
|
1133
|
+
stop_reactor
|
1134
|
+
end
|
1135
|
+
channel.publish(message_name)
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
context 'when credentials are invalid' do
|
1140
|
+
let(:key_secret) { 'invalid' }
|
1141
|
+
let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body }
|
1142
|
+
|
1143
|
+
it 'fails with an invalid signature error' do
|
1144
|
+
client.connection.once(:disconnected) do |state_change|
|
1145
|
+
expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
|
1146
|
+
expect(state_change.reason.code).to eql(40144)
|
1147
|
+
stop_reactor
|
1148
|
+
end
|
1149
|
+
client.connect
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
context 'when JWT token expires' do
|
1155
|
+
before do
|
1156
|
+
stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire
|
1157
|
+
stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) # Ensure tokens issued expire immediately after issue
|
1158
|
+
end
|
1159
|
+
let(:token_callback) do
|
1160
|
+
lambda do |token_params|
|
1161
|
+
# Ably in all environments other than production will send AUTH 5 seconds before expiry, so
|
1162
|
+
# we generate a JWT that expires in 5s so that the window for Realtime to send has passed
|
1163
|
+
tokenResponse = Faraday.get "#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&expiresIn=5"
|
1164
|
+
tokenResponse.body
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
let(:client_options) { default_options.merge(use_token_auth: true, auth_callback: token_callback) }
|
1168
|
+
|
1169
|
+
# RTC8a
|
1170
|
+
it 'client disconnects, a new token is requested via auth_callback and the client gets reconnected' do
|
1171
|
+
client.connection.once(:connected) do
|
1172
|
+
original_token = auth.current_token_details
|
1173
|
+
original_conn_id = client.connection.id
|
1174
|
+
|
1175
|
+
client.connection.once(:disconnected) do |state_change|
|
1176
|
+
expect(state_change.reason.code).to eql(40142)
|
1177
|
+
|
1178
|
+
client.connection.once(:connected) do
|
1179
|
+
expect(original_token).to_not eql(auth.current_token_details)
|
1180
|
+
expect(original_conn_id).to eql(client.connection.id)
|
1181
|
+
stop_reactor
|
1182
|
+
end
|
1183
|
+
end
|
1184
|
+
end
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
context 'and an AUTH procol message is received' do
|
1188
|
+
let(:token_callback) do
|
1189
|
+
lambda do |token_params|
|
1190
|
+
# Ably in all environments other than local will send AUTH 30 seconds before expiry
|
1191
|
+
# We set the TTL to 35s so there's room to receive an AUTH protocol message
|
1192
|
+
tokenResponse = Faraday.get "#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&expiresIn=35"
|
1193
|
+
tokenResponse.body
|
1194
|
+
end
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
# RTC8a, RTC8a4
|
1198
|
+
it 'client reauths correctly without going through a disconnection' do
|
1199
|
+
client.connection.once(:connected) do
|
1200
|
+
original_token = client.auth.current_token_details
|
1201
|
+
received_auth = false
|
1202
|
+
|
1203
|
+
client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
|
1204
|
+
received_auth = true if protocol_message.action == :auth
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
client.connection.once(:update) do
|
1208
|
+
expect(received_auth).to be_truthy
|
1209
|
+
expect(original_token).to_not eql(client.auth.current_token_details)
|
1210
|
+
stop_reactor
|
1211
|
+
end
|
1212
|
+
end
|
1213
|
+
end
|
1214
|
+
end
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
context 'when the JWT token request includes a client_id' do
|
1218
|
+
let(:client_id) { random_str }
|
1219
|
+
let(:auth_callback) do
|
1220
|
+
lambda do |token_params|
|
1221
|
+
Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&client_id=#{client_id}").body
|
1222
|
+
end
|
1223
|
+
end
|
1224
|
+
let(:client_options) { default_options.merge(auth_callback: auth_callback) }
|
1225
|
+
|
1226
|
+
it 'the client_id is the same that was specified in the auth_callback that generated the JWT token' do
|
1227
|
+
client.connection.once(:connected) do
|
1228
|
+
expect(client.auth.client_id).to eql(client_id)
|
1229
|
+
stop_reactor
|
1230
|
+
end
|
1231
|
+
end
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
context 'when the JWT token request includes a subscribe-only capability' do
|
1235
|
+
let(:channel_with_publish_permissions) { "test_JWT_with_publish_#{random_str}" }
|
1236
|
+
let(:basic_capability) { JSON.dump(channel_name => ['subscribe'], channel_with_publish_permissions => ['publish']) }
|
1237
|
+
let(:auth_callback) do
|
1238
|
+
lambda do |token_params|
|
1239
|
+
Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&capability=#{URI.escape(basic_capability)}").body
|
1240
|
+
end
|
1241
|
+
end
|
1242
|
+
let(:client_options) { default_options.merge(auth_callback: auth_callback) }
|
1243
|
+
|
1244
|
+
it 'client fails to publish to a channel with subscribe-only capability and publishes successfully on a channel with permissions' do
|
1245
|
+
client.connection.once(:connected) do
|
1246
|
+
forbidden_channel = client.channels.get(channel_name)
|
1247
|
+
allowed_channel = client.channels.get(channel_with_publish_permissions)
|
1248
|
+
forbidden_channel.publish('not-allowed').errback do |error|
|
1249
|
+
expect(error.code).to eql(40160)
|
1250
|
+
expect(error.message).to match(/permission denied/)
|
1251
|
+
|
1252
|
+
allowed_channel.publish(message_name) do |message|
|
1253
|
+
expect(message.name).to eql(message_name)
|
1254
|
+
stop_reactor
|
1255
|
+
end
|
1256
|
+
end
|
1257
|
+
end
|
1258
|
+
end
|
1259
|
+
end
|
1260
|
+
end
|
1034
1261
|
end
|
1035
1262
|
end
|
@@ -1338,5 +1338,42 @@ describe Ably::Auth do
|
|
1338
1338
|
expect(response).to be_a(Ably::Models::TokenDetails)
|
1339
1339
|
end
|
1340
1340
|
end
|
1341
|
+
|
1342
|
+
# RSC1, RSC1a, RSA3c, RSA3d
|
1343
|
+
context 'when using JWT' do
|
1344
|
+
let(:auth_url) { 'https://echo.ably.io/createJWT' }
|
1345
|
+
let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body }
|
1346
|
+
let(:client) { Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol) }
|
1347
|
+
|
1348
|
+
it 'authenticates correctly using the JWT token generated by the echo server' do
|
1349
|
+
expect(client.stats).to_not be_nil()
|
1350
|
+
end
|
1351
|
+
|
1352
|
+
context 'when the JWT embeds an Ably token' do
|
1353
|
+
let(:token) { Faraday.post(auth_url, { keyName: key_name, keySecret: key_secret, jwtType: :embedded }).body }
|
1354
|
+
|
1355
|
+
it 'authenticates correctly using the embedded token' do
|
1356
|
+
expect(client.stats).to_not be_nil()
|
1357
|
+
end
|
1358
|
+
|
1359
|
+
context 'and the requested token is encrypted' do
|
1360
|
+
let(:token) { Faraday.post(auth_url, { keyName: key_name, keySecret: key_secret, jwtType: :embedded, encrypted: 1 }).body }
|
1361
|
+
|
1362
|
+
it 'authenticates correctly using the embedded token' do
|
1363
|
+
expect(client.stats).to_not be_nil()
|
1364
|
+
end
|
1365
|
+
end
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
# RSA4f, RSA8c
|
1369
|
+
context 'when the token requested is returned with application/jwt content type' do
|
1370
|
+
let(:auth_rest_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) }
|
1371
|
+
let(:auth_params) { { keyName: key_name, keySecret: key_secret, returnType: 'jwt' } }
|
1372
|
+
let(:token) { auth_rest_client.auth.request_token({ }, { auth_url: auth_url, auth_params: auth_params }).token }
|
1373
|
+
it 'authenticates correctly and pulls stats' do
|
1374
|
+
expect(client.stats).to_not be_nil()
|
1375
|
+
end
|
1376
|
+
end
|
1377
|
+
end
|
1341
1378
|
end
|
1342
1379
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ably
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lewis Marshall
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-
|
12
|
+
date: 2018-06-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: eventmachine
|