stormpath-sdk 1.0.0.beta.7 → 1.0.0.beta.8
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 +7 -0
- data/.travis.yml +8 -29
- data/CHANGES.md +10 -0
- data/README.md +64 -0
- data/lib/stormpath-sdk.rb +5 -1
- data/lib/stormpath-sdk/api_key.rb +0 -3
- data/lib/stormpath-sdk/auth/basic_authenticator.rb +1 -1
- data/lib/stormpath-sdk/auth/username_password_request.rb +1 -1
- data/lib/stormpath-sdk/client.rb +16 -17
- data/lib/stormpath-sdk/data_store.rb +4 -3
- data/lib/stormpath-sdk/error.rb +19 -18
- data/lib/stormpath-sdk/http/authc/sauthc1_signer.rb +18 -28
- data/lib/stormpath-sdk/http/http_client_request_executor.rb +5 -16
- data/lib/stormpath-sdk/http/request.rb +3 -2
- data/lib/stormpath-sdk/http/response.rb +3 -3
- data/lib/stormpath-sdk/id_site/id_site_result.rb +17 -0
- data/lib/stormpath-sdk/resource/application.rb +35 -1
- data/lib/stormpath-sdk/resource/collection.rb +1 -1
- data/lib/stormpath-sdk/resource/directory.rb +2 -0
- data/lib/stormpath-sdk/resource/group.rb +1 -1
- data/lib/stormpath-sdk/resource/tenant.rb +2 -0
- data/lib/stormpath-sdk/util/assert.rb +4 -4
- data/lib/stormpath-sdk/version.rb +2 -2
- data/spec/auth/basic_authenticator_spec.rb +1 -1
- data/spec/auth/sauthc1_signer_spec.rb +4 -4
- data/spec/client_spec.rb +1 -1
- data/spec/data_store_spec.rb +1 -1
- data/spec/provider/account_resolver_spec.rb +1 -1
- data/spec/resource/application_spec.rb +176 -0
- data/spec/resource/collection_spec.rb +4 -4
- data/spec/resource/directory_spec.rb +18 -0
- data/spec/resource/group_spec.rb +11 -0
- data/spec/resource/tenant_spec.rb +12 -0
- data/spec/spec_helper.rb +5 -1
- data/stormpath-sdk.gemspec +1 -0
- metadata +61 -81
- data/lib/stormpath-sdk/ext/hash.rb +0 -31
@@ -18,9 +18,9 @@ module Stormpath
|
|
18
18
|
class Request
|
19
19
|
include Stormpath::Http::Utils
|
20
20
|
|
21
|
-
attr_accessor :http_method, :href, :query_string, :http_headers, :body
|
21
|
+
attr_accessor :http_method, :href, :query_string, :http_headers, :body, :api_key
|
22
22
|
|
23
|
-
def initialize(http_method, href, query_string, http_headers, body)
|
23
|
+
def initialize(http_method, href, query_string, http_headers, body, api_key)
|
24
24
|
|
25
25
|
splitted = href.split '?'
|
26
26
|
|
@@ -41,6 +41,7 @@ module Stormpath
|
|
41
41
|
@http_method = http_method.upcase
|
42
42
|
@http_headers = http_headers
|
43
43
|
@body = body
|
44
|
+
@api_key = api_key
|
44
45
|
|
45
46
|
if body
|
46
47
|
@http_headers.store 'Content-Length', @body.bytesize
|
@@ -17,8 +17,8 @@ module Stormpath
|
|
17
17
|
module Http
|
18
18
|
class Response
|
19
19
|
|
20
|
-
attr_reader :http_status, :
|
21
|
-
|
20
|
+
attr_reader :http_status, :body
|
21
|
+
attr_accessor :headers
|
22
22
|
|
23
23
|
def initialize http_status, content_type, body, content_length
|
24
24
|
@http_status = http_status
|
@@ -42,4 +42,4 @@ module Stormpath
|
|
42
42
|
|
43
43
|
end
|
44
44
|
end
|
45
|
-
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Stormpath
|
2
|
+
module IdSite
|
3
|
+
class IdSiteResult
|
4
|
+
attr_accessor :account_href, :state, :status, :is_new_account
|
5
|
+
|
6
|
+
alias_method :new_account?, :is_new_account
|
7
|
+
|
8
|
+
def initialize(jwt_response)
|
9
|
+
@account_href = jwt_response["sub"]
|
10
|
+
@status = jwt_response["status"]
|
11
|
+
@state = jwt_response["state"]
|
12
|
+
@is_new_account = jwt_response["isNewSub"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -15,6 +15,8 @@
|
|
15
15
|
#
|
16
16
|
class Stormpath::Resource::Application < Stormpath::Resource::Instance
|
17
17
|
include Stormpath::Resource::Status
|
18
|
+
include Stormpath::Resource::CustomDataStorage
|
19
|
+
include UUIDTools
|
18
20
|
|
19
21
|
class LoadError < Stormpath::Error; end
|
20
22
|
|
@@ -26,9 +28,10 @@ class Stormpath::Resource::Application < Stormpath::Resource::Instance
|
|
26
28
|
has_many :password_reset_tokens, can: [:get, :create]
|
27
29
|
has_many :account_store_mappings, can: [:get, :create]
|
28
30
|
has_many :groups, can: [:get, :create]
|
29
|
-
|
31
|
+
|
30
32
|
has_one :default_account_store_mapping, class_name: :accountStoreMapping
|
31
33
|
has_one :default_group_store_mapping, class_name: :accountStoreMapping
|
34
|
+
has_one :custom_data
|
32
35
|
|
33
36
|
def self.load composite_url
|
34
37
|
begin
|
@@ -47,6 +50,37 @@ class Stormpath::Resource::Application < Stormpath::Resource::Instance
|
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
53
|
+
def create_id_site_url(options = {})
|
54
|
+
base = client.data_store.base_url.sub("v" + Stormpath::DataStore::DEFAULT_API_VERSION.to_s, "sso")
|
55
|
+
base += '/logout' if options[:logout]
|
56
|
+
|
57
|
+
token = JWT.encode({
|
58
|
+
'iat' => Time.now.to_i,
|
59
|
+
'jti' => UUID.method(:random_create).call.to_s,
|
60
|
+
'iss' => client.data_store.api_key.id,
|
61
|
+
'sub' => href,
|
62
|
+
'cb_uri' => options[:callback_uri],
|
63
|
+
'path' => options[:path] || '',
|
64
|
+
'state' => options[:state] || ''
|
65
|
+
}, client.data_store.api_key.secret, 'HS256')
|
66
|
+
|
67
|
+
base + '?jwtRequest=' + token
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_id_site_callback(response_url)
|
71
|
+
assert_not_nil response_url, "No response provided. Please provide response object."
|
72
|
+
|
73
|
+
uri = URI(response_url)
|
74
|
+
params = CGI::parse(uri.query)
|
75
|
+
token = params["jwtResponse"].first
|
76
|
+
|
77
|
+
jwt_response, _header = JWT.decode(token, client.data_store.api_key.secret)
|
78
|
+
|
79
|
+
raise Stormpath::Error.new if jwt_response["aud"] != client.data_store.api_key.id
|
80
|
+
|
81
|
+
Stormpath::IdSite::IdSiteResult.new(jwt_response)
|
82
|
+
end
|
83
|
+
|
50
84
|
def send_password_reset_email email
|
51
85
|
password_reset_token = create_password_reset_token email;
|
52
86
|
password_reset_token.account
|
@@ -15,6 +15,7 @@
|
|
15
15
|
#
|
16
16
|
class Stormpath::Resource::Directory < Stormpath::Resource::Instance
|
17
17
|
include Stormpath::Resource::Status
|
18
|
+
include Stormpath::Resource::CustomDataStorage
|
18
19
|
|
19
20
|
prop_accessor :name, :description
|
20
21
|
|
@@ -22,6 +23,7 @@ class Stormpath::Resource::Directory < Stormpath::Resource::Instance
|
|
22
23
|
|
23
24
|
has_many :accounts, can: [:get, :create]
|
24
25
|
has_many :groups, can: [:get, :create]
|
26
|
+
has_one :custom_data
|
25
27
|
|
26
28
|
def create_account account, registration_workflow_enabled=nil
|
27
29
|
href = accounts.href
|
@@ -14,10 +14,12 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
class Stormpath::Resource::Tenant < Stormpath::Resource::Instance
|
17
|
+
include Stormpath::Resource::CustomDataStorage
|
17
18
|
|
18
19
|
prop_reader :name, :key
|
19
20
|
|
20
21
|
has_many :applications
|
21
22
|
has_many :directories
|
23
|
+
has_one :custom_data
|
22
24
|
|
23
25
|
end
|
@@ -18,19 +18,19 @@ module Stormpath
|
|
18
18
|
module Assert
|
19
19
|
|
20
20
|
def assert_not_nil(object, message)
|
21
|
-
raise
|
21
|
+
raise(ArgumentError, message, caller) if object.nil?
|
22
22
|
end
|
23
23
|
|
24
24
|
def assert_kind_of(clazz, object, message)
|
25
|
-
raise
|
25
|
+
raise(ArgumentError, message, caller) unless object.kind_of? clazz
|
26
26
|
end
|
27
27
|
|
28
28
|
def assert_true(arg, message)
|
29
|
-
raise
|
29
|
+
raise(ArgumentError, message, caller) unless arg
|
30
30
|
end
|
31
31
|
|
32
32
|
def assert_false(arg, message)
|
33
|
-
raise
|
33
|
+
raise(ArgumentError, message, caller) if arg
|
34
34
|
end
|
35
35
|
|
36
36
|
end
|
@@ -4,7 +4,7 @@ describe "BasicAuthenticator" do
|
|
4
4
|
context "given an instance of BasicAuthenticator" do
|
5
5
|
|
6
6
|
before do
|
7
|
-
data_store = Stormpath::DataStore.new "", {}, ""
|
7
|
+
data_store = Stormpath::DataStore.new "", "", {}, ""
|
8
8
|
allow(test_api_client).to receive(:data_store).and_return(data_store)
|
9
9
|
auth_result = Stormpath::Authentication::AuthenticationResult.new({}, test_api_client)
|
10
10
|
allow(data_store).to receive(:create).and_return(auth_result)
|
@@ -19,17 +19,17 @@ describe Stormpath::Http::Authc::Sauthc1Signer do
|
|
19
19
|
let(:fake_api_key) { Stormpath::ApiKey.new('foo', 'bar') }
|
20
20
|
|
21
21
|
let(:empty_query_hash_request) do
|
22
|
-
Stormpath::Http::Request.new 'get', 'http://example.com/resources/abc123?q=red blue', nil, Hash.new, nil
|
22
|
+
Stormpath::Http::Request.new 'get', 'http://example.com/resources/abc123?q=red blue', nil, Hash.new, nil, test_api_key
|
23
23
|
end
|
24
24
|
|
25
25
|
let(:filled_query_hash_request) do
|
26
|
-
Stormpath::Http::Request.new 'get', 'http://example.com/resources/abc123', {'q' => 'red blue'}, Hash.new, nil
|
26
|
+
Stormpath::Http::Request.new 'get', 'http://example.com/resources/abc123', {'q' => 'red blue'}, Hash.new, nil, test_api_key
|
27
27
|
end
|
28
28
|
|
29
29
|
before do
|
30
30
|
Timecop.freeze(Time.now)
|
31
|
-
signer.sign_request empty_query_hash_request
|
32
|
-
signer.sign_request filled_query_hash_request
|
31
|
+
signer.sign_request empty_query_hash_request
|
32
|
+
signer.sign_request filled_query_hash_request
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'assigns identical headers to both requests' do
|
data/spec/client_spec.rb
CHANGED
@@ -262,7 +262,7 @@ properties
|
|
262
262
|
it 'initializes the request executor with the proxy' do
|
263
263
|
expect(Stormpath::Http::HttpClientRequestExecutor)
|
264
264
|
.to receive(:new)
|
265
|
-
.with(
|
265
|
+
.with(proxy: http_proxy)
|
266
266
|
.and_return request_executor
|
267
267
|
|
268
268
|
Stormpath::Client.new api_key: api_key, proxy: http_proxy
|
data/spec/data_store_spec.rb
CHANGED
@@ -4,7 +4,7 @@ describe Stormpath::DataStore do
|
|
4
4
|
let(:factory) { Stormpath::Test::ResourceFactory.new }
|
5
5
|
let(:request_executor) { Stormpath::Test::TestRequestExecutor.new }
|
6
6
|
let(:store) { Stormpath::Cache::RedisStore }
|
7
|
-
let(:data_store) { Stormpath::DataStore.new request_executor, {store: store}, nil, nil }
|
7
|
+
let(:data_store) { Stormpath::DataStore.new request_executor, test_api_key, {store: store}, nil, nil }
|
8
8
|
let(:application_cache) { data_store.cache_manager.get_cache 'applications' }
|
9
9
|
let(:tenant_cache) { data_store.cache_manager.get_cache 'tenants' }
|
10
10
|
let(:group_cache) { data_store.cache_manager.get_cache 'groups' }
|
@@ -4,7 +4,7 @@ describe "ProviderAccountResolver" do
|
|
4
4
|
context "given an instance of ProviderAccountResolver" do
|
5
5
|
|
6
6
|
before do
|
7
|
-
data_store = Stormpath::DataStore.new "", {}, ""
|
7
|
+
data_store = Stormpath::DataStore.new "", "", {}, ""
|
8
8
|
allow(test_api_client).to receive(:data_store).and_return(data_store)
|
9
9
|
auth_result = Stormpath::Provider::AccountResult.new({}, test_api_client)
|
10
10
|
allow(data_store).to receive(:create).and_return(auth_result)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
include UUIDTools
|
2
3
|
|
3
4
|
describe Stormpath::Resource::Application, :vcr do
|
4
5
|
let(:app) { test_api_client.applications.create name: random_application_name, description: 'Dummy desc.' }
|
@@ -28,6 +29,7 @@ describe Stormpath::Resource::Application, :vcr do
|
|
28
29
|
expect(application.tenant).to be_a Stormpath::Resource::Tenant
|
29
30
|
expect(application.default_account_store_mapping).to be_a Stormpath::Resource::AccountStoreMapping
|
30
31
|
expect(application.default_group_store_mapping).to be_a Stormpath::Resource::AccountStoreMapping
|
32
|
+
expect(application.custom_data).to be_a Stormpath::Resource::CustomData
|
31
33
|
|
32
34
|
expect(application.groups).to be_a Stormpath::Resource::Collection
|
33
35
|
expect(application.accounts).to be_a Stormpath::Resource::Collection
|
@@ -288,4 +290,178 @@ describe Stormpath::Resource::Application, :vcr do
|
|
288
290
|
end
|
289
291
|
end
|
290
292
|
end
|
293
|
+
|
294
|
+
describe '#create_application_with_custom_data' do
|
295
|
+
it 'creates an application with custom data' do
|
296
|
+
application.custom_data["category"] = "classified"
|
297
|
+
application.save
|
298
|
+
|
299
|
+
expect(application.custom_data["category"]).to eq("classified")
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
describe '#create_id_site_url' do
|
304
|
+
let(:jwt_token) { JWT.encode({
|
305
|
+
'iat' => Time.now.to_i,
|
306
|
+
'jti' => UUID.method(:random_create).call.to_s,
|
307
|
+
'aud' => test_api_key_id,
|
308
|
+
'sub' => application.href,
|
309
|
+
'cb_uri' => 'http://localhost:9292/redirect',
|
310
|
+
'path' => '',
|
311
|
+
'state' => ''
|
312
|
+
}, test_api_key_secret, 'HS256')
|
313
|
+
}
|
314
|
+
|
315
|
+
let(:create_id_site_url_result) do
|
316
|
+
options = { callback_uri: 'http://localhost:9292/redirect' }
|
317
|
+
application.create_id_site_url options
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'should create a url with jwtRequest' do
|
321
|
+
expect(create_id_site_url_result).to include('jwtRequest')
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'should create a request to /sso' do
|
325
|
+
expect(create_id_site_url_result).to include('/sso')
|
326
|
+
end
|
327
|
+
|
328
|
+
it 'should create a jwtRequest that is signed wit the client secret' do
|
329
|
+
uri = Addressable::URI.parse(create_id_site_url_result)
|
330
|
+
jwt_token = JWT.decode(uri.query_values["jwtRequest"], test_api_key_secret).first
|
331
|
+
|
332
|
+
expect(jwt_token["iss"]).to eq test_api_key_id
|
333
|
+
expect(jwt_token["sub"]).to eq application.href
|
334
|
+
expect(jwt_token["cb_uri"]).to eq 'http://localhost:9292/redirect'
|
335
|
+
end
|
336
|
+
|
337
|
+
context 'with logout option' do
|
338
|
+
it 'shoud create a request to /sso/logout' do
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
describe '#handle_id_site_callback' do
|
344
|
+
let(:callback_uri_base) { 'http://localhost:9292/somwhere?jwtResponse=' }
|
345
|
+
|
346
|
+
context 'without the response_url provided' do
|
347
|
+
it 'should raise argument error' do
|
348
|
+
expect { application.handle_id_site_callback(nil) }.to raise_error(ArgumentError)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
context 'with a valid jwt response' do
|
353
|
+
let(:jwt_token) { JWT.encode({
|
354
|
+
'iat' => Time.now.to_i,
|
355
|
+
'aud' => test_api_key_id,
|
356
|
+
'sub' => application.href,
|
357
|
+
'path' => '',
|
358
|
+
'state' => '',
|
359
|
+
'isNewSub' => true,
|
360
|
+
'status' => "REGISTERED"
|
361
|
+
}, test_api_key_secret, 'HS256')
|
362
|
+
}
|
363
|
+
|
364
|
+
before do
|
365
|
+
@site_result = application.handle_id_site_callback(callback_uri_base + jwt_token)
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'should return IdSiteResult object' do
|
369
|
+
expect(@site_result).to be_kind_of(Stormpath::IdSite::IdSiteResult)
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'should set the correct account on IdSiteResult object' do
|
373
|
+
expect(@site_result.account_href).to eq(application.href)
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'should set the correct status on IdSiteResult object' do
|
377
|
+
expect(@site_result.status).to eq("REGISTERED")
|
378
|
+
end
|
379
|
+
|
380
|
+
it 'should set the correct state on IdSiteResult object' do
|
381
|
+
expect(@site_result.state).to eq("")
|
382
|
+
end
|
383
|
+
|
384
|
+
it 'should set the correct is_new_account on IdSiteResult object' do
|
385
|
+
expect(@site_result.new_account?).to eq(true)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
context 'with an expired token' do
|
390
|
+
let(:jwt_token) { JWT.encode({
|
391
|
+
'iat' => Time.now.to_i,
|
392
|
+
'aud' => test_api_key_id,
|
393
|
+
'sub' => application.href,
|
394
|
+
'path' => '',
|
395
|
+
'state' => '',
|
396
|
+
'exp' => Time.now.to_i - 1,
|
397
|
+
'isNewSub' => true,
|
398
|
+
'status' => "REGISTERED"
|
399
|
+
}, test_api_key_secret, 'HS256')
|
400
|
+
}
|
401
|
+
|
402
|
+
it 'should raise expiration error' do
|
403
|
+
expect {
|
404
|
+
application.handle_id_site_callback(callback_uri_base + jwt_token)
|
405
|
+
}.to raise_error(JWT::DecodeError)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
context 'with a different client id (aud)' do
|
410
|
+
let(:jwt_token) { JWT.encode({
|
411
|
+
'iat' => Time.now.to_i,
|
412
|
+
'aud' => UUID.method(:random_create).call.to_s,
|
413
|
+
'sub' => application.href,
|
414
|
+
'path' => '',
|
415
|
+
'state' => '',
|
416
|
+
'isNewSub' => true,
|
417
|
+
'status' => "REGISTERED"
|
418
|
+
}, test_api_key_secret, 'HS256')
|
419
|
+
}
|
420
|
+
|
421
|
+
it 'should raise error' do
|
422
|
+
expect {
|
423
|
+
application.handle_id_site_callback(callback_uri_base + jwt_token)
|
424
|
+
}.to raise_error(Stormpath::Error)
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
context 'with an invalid exp value' do
|
429
|
+
let(:jwt_token) { JWT.encode({
|
430
|
+
'iat' => Time.now.to_i,
|
431
|
+
'aud' => test_api_key_id,
|
432
|
+
'sub' => application.href,
|
433
|
+
'path' => '',
|
434
|
+
'state' => '',
|
435
|
+
'exp' => 'not gona work',
|
436
|
+
'isNewSub' => true,
|
437
|
+
'status' => "REGISTERED"
|
438
|
+
}, test_api_key_secret, 'HS256')
|
439
|
+
}
|
440
|
+
|
441
|
+
it 'should error with the expiration error' do
|
442
|
+
expect {
|
443
|
+
application.handle_id_site_callback(callback_uri_base + jwt_token)
|
444
|
+
}.to raise_error(JWT::DecodeError)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
context 'with an invalid signature' do
|
449
|
+
let(:jwt_token) { JWT.encode({
|
450
|
+
'iat' => Time.now.to_i,
|
451
|
+
'aud' => test_api_key_id,
|
452
|
+
'sub' => application.href,
|
453
|
+
'path' => '',
|
454
|
+
'state' => '',
|
455
|
+
'isNewSub' => true,
|
456
|
+
'status' => "REGISTERED"
|
457
|
+
}, 'false key', 'HS256')
|
458
|
+
}
|
459
|
+
|
460
|
+
it 'should reject the signature' do
|
461
|
+
expect {
|
462
|
+
application.handle_id_site_callback(callback_uri_base + jwt_token)
|
463
|
+
}.to raise_error(JWT::DecodeError)
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
291
467
|
end
|