stormpath-sdk 1.0.0.beta.7 → 1.0.0.beta.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|