ably 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|