osso 0.0.3.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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.buildkite/hooks/environment +9 -0
  3. data/.buildkite/hooks/pre-command +7 -0
  4. data/.buildkite/pipeline.yml +6 -0
  5. data/.buildkite/template.yml +5 -0
  6. data/.gitignore +10 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +81 -0
  9. data/CODE_OF_CONDUCT.md +130 -0
  10. data/Gemfile +18 -0
  11. data/Gemfile.lock +176 -0
  12. data/LICENSE +111 -0
  13. data/README.md +2 -0
  14. data/Rakefile +14 -0
  15. data/bin/console +8 -0
  16. data/bin/setup +8 -0
  17. data/config/database.yml +14 -0
  18. data/db/schema.rb +133 -0
  19. data/lib/osso.rb +11 -0
  20. data/lib/osso/Rakefile +13 -0
  21. data/lib/osso/db/migrate/20190909230109_enable_uuid.rb +7 -0
  22. data/lib/osso/db/migrate/20200328135750_create_users.rb +12 -0
  23. data/lib/osso/db/migrate/20200328143303_create_oauth_tables.rb +57 -0
  24. data/lib/osso/db/migrate/20200328143305_create_identity_providers.rb +12 -0
  25. data/lib/osso/db/migrate/20200411184535_add_provider_id_to_users.rb +7 -0
  26. data/lib/osso/db/migrate/20200411192645_create_enterprise_accounts.rb +15 -0
  27. data/lib/osso/db/migrate/20200413132407_add_oauth_clients.rb +13 -0
  28. data/lib/osso/db/migrate/20200413142511_create_authorization_codes.rb +15 -0
  29. data/lib/osso/db/migrate/20200413163451_create_access_tokens.rb +13 -0
  30. data/lib/osso/db/migrate/20200502120616_create_redirect_uris_and_drop_from_oauth_clients.rb +13 -0
  31. data/lib/osso/db/migrate/20200502135008_add_oauth_client_id_to_enterprise_accounts_and_identity_providers.rb +6 -0
  32. data/lib/osso/db/migrate/20200714223226_add_identity_provider_service_enum.rb +17 -0
  33. data/lib/osso/db/migrate/20200715154211_rename_idp_fields_on_identity_provider_to_sso.rb +6 -0
  34. data/lib/osso/db/migrate/20200715205801_add_name_to_enterprise_account.rb +5 -0
  35. data/lib/osso/graphql/mutation.rb +16 -0
  36. data/lib/osso/graphql/mutations.rb +12 -0
  37. data/lib/osso/graphql/mutations/base_mutation.rb +41 -0
  38. data/lib/osso/graphql/mutations/configure_identity_provider.rb +36 -0
  39. data/lib/osso/graphql/mutations/create_enterprise_account.rb +25 -0
  40. data/lib/osso/graphql/mutations/create_identity_provider.rb +30 -0
  41. data/lib/osso/graphql/mutations/set_identity_provider.rb +27 -0
  42. data/lib/osso/graphql/query.rb +25 -0
  43. data/lib/osso/graphql/resolvers.rb +12 -0
  44. data/lib/osso/graphql/resolvers/enterprise_account.rb +25 -0
  45. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +17 -0
  46. data/lib/osso/graphql/resolvers/oauth_clients.rb +15 -0
  47. data/lib/osso/graphql/schema.rb +46 -0
  48. data/lib/osso/graphql/types.rb +15 -0
  49. data/lib/osso/graphql/types/base_enum.rb +10 -0
  50. data/lib/osso/graphql/types/base_input_object.rb +10 -0
  51. data/lib/osso/graphql/types/base_object.rb +12 -0
  52. data/lib/osso/graphql/types/enterprise_account.rb +33 -0
  53. data/lib/osso/graphql/types/identity_provider.rb +37 -0
  54. data/lib/osso/graphql/types/identity_provider_service.rb +12 -0
  55. data/lib/osso/graphql/types/oauth_client.rb +20 -0
  56. data/lib/osso/graphql/types/user.rb +17 -0
  57. data/lib/osso/helpers/auth.rb +71 -0
  58. data/lib/osso/helpers/helpers.rb +8 -0
  59. data/lib/osso/lib/app_config.rb +20 -0
  60. data/lib/osso/lib/oauth2_token.rb +38 -0
  61. data/lib/osso/lib/route_map.rb +28 -0
  62. data/lib/osso/models/access_token.rb +29 -0
  63. data/lib/osso/models/authorization_code.rb +14 -0
  64. data/lib/osso/models/enterprise_account.rb +28 -0
  65. data/lib/osso/models/identity_provider.rb +48 -0
  66. data/lib/osso/models/models.rb +16 -0
  67. data/lib/osso/models/oauth_client.rb +32 -0
  68. data/lib/osso/models/redirect_uri.rb +20 -0
  69. data/lib/osso/models/saml_provider.rb +49 -0
  70. data/lib/osso/models/saml_providers/azure_saml_provider.rb +22 -0
  71. data/lib/osso/models/saml_providers/okta_saml_provider.rb +23 -0
  72. data/lib/osso/models/user.rb +24 -0
  73. data/lib/osso/rake.rb +4 -0
  74. data/lib/osso/routes/admin.rb +41 -0
  75. data/lib/osso/routes/auth.rb +67 -0
  76. data/lib/osso/routes/oauth.rb +63 -0
  77. data/lib/osso/routes/routes.rb +10 -0
  78. data/lib/osso/routes/views/error.erb +1 -0
  79. data/lib/osso/routes/views/multiple_providers.erb +1 -0
  80. data/lib/osso/version.rb +5 -0
  81. data/lib/tasks/bootstrap.rake +16 -0
  82. data/osso-rb.gemspec +40 -0
  83. data/spec/factories/authorization_code.rb +10 -0
  84. data/spec/factories/enterprise_account.rb +46 -0
  85. data/spec/factories/identity_providers.rb +49 -0
  86. data/spec/factories/oauth_client.rb +12 -0
  87. data/spec/factories/redirect_uri.rb +14 -0
  88. data/spec/factories/user.rb +18 -0
  89. data/spec/graphql/mutations/configure_identity_provider_spec.rb +75 -0
  90. data/spec/graphql/mutations/create_enterprise_account_spec.rb +68 -0
  91. data/spec/graphql/mutations/create_identity_provider_spec.rb +104 -0
  92. data/spec/graphql/query/enterprise_account_spec.rb +68 -0
  93. data/spec/graphql/query/enterprise_accounts_spec.rb +44 -0
  94. data/spec/graphql/query/identity_provider_spec.rb +65 -0
  95. data/spec/graphql/query/oauth_clients_account_spec.rb +48 -0
  96. data/spec/models/azure_saml_provider_spec.rb +19 -0
  97. data/spec/models/identity_provider_spec.rb +17 -0
  98. data/spec/models/okta_saml_provider_spec.rb +20 -0
  99. data/spec/routes/admin_spec.rb +60 -0
  100. data/spec/routes/app_spec.rb +6 -0
  101. data/spec/routes/auth_spec.rb +112 -0
  102. data/spec/routes/oauth_spec.rb +134 -0
  103. data/spec/spec_helper.rb +68 -0
  104. data/spec/support/spec_app.rb +9 -0
  105. data/spec/support/views/admin.erb +5 -0
  106. metadata +348 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::GraphQL::Schema do
6
+ describe 'EnterpriseAccounts' do
7
+ describe 'for an admin user' do
8
+ let(:current_scope) { :admin }
9
+
10
+ it 'returns Enterprise Accounts' do
11
+ create_list(:enterprise_account, 2)
12
+
13
+ query = <<~GRAPHQL
14
+ query EnterpriseAccounts {
15
+ enterpriseAccounts {
16
+ domain
17
+ id
18
+ identityProviders {
19
+ id
20
+ service
21
+ domain
22
+ acsUrl
23
+ ssoCert
24
+ ssoUrl
25
+ configured
26
+ }
27
+ name
28
+ status
29
+ }
30
+ }
31
+ GRAPHQL
32
+
33
+ response = described_class.execute(
34
+ query,
35
+ variables: nil,
36
+ context: { scope: current_scope },
37
+ )
38
+
39
+ expect(response['errors']).to be_nil
40
+ expect(response.dig('data', 'enterpriseAccounts').count).to eq(2)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::GraphQL::Schema do
6
+ describe 'Identity Provider' do
7
+ let(:id) { Faker::Internet.uuid }
8
+ let(:domain) { Faker::Internet.domain_name }
9
+ let(:variables) { { id: id } }
10
+ let(:query) do
11
+ <<~GRAPHQL
12
+ query IdentityProvider($id: ID!) {
13
+ identityProvider(id: $id) {
14
+ id
15
+ service
16
+ domain
17
+ acsUrl
18
+ ssoCert
19
+ ssoUrl
20
+ configured
21
+ }
22
+ }
23
+ GRAPHQL
24
+ end
25
+
26
+ before do
27
+ create(:identity_provider)
28
+ create(:identity_provider, id: id, domain: domain)
29
+ end
30
+
31
+ subject do
32
+ described_class.execute(
33
+ query,
34
+ variables: variables,
35
+ context: { scope: current_scope },
36
+ )
37
+ end
38
+
39
+ describe 'for an admin user' do
40
+ let(:current_scope) { :admin }
41
+ it 'returns Identity Provider for id' do
42
+ expect(subject['errors']).to be_nil
43
+ expect(subject.dig('data', 'identityProvider', 'id')).to eq(id)
44
+ end
45
+ end
46
+
47
+ describe 'for an email scoped user' do
48
+ let(:current_scope) { domain }
49
+
50
+ it 'returns Enterprise Account for domain' do
51
+ expect(subject['errors']).to be_nil
52
+ expect(subject.dig('data', 'identityProvider', 'domain')).to eq(domain)
53
+ end
54
+ end
55
+
56
+ describe 'for the wrong email scoped user' do
57
+ let(:current_scope) { 'bar.com' }
58
+
59
+ it 'returns Enterprise Account for domain' do
60
+ expect(subject['errors']).to_not be_empty
61
+ expect(subject.dig('data', 'enterpriseAccount')).to be_nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::GraphQL::Schema do
6
+ describe 'OAuthClients' do
7
+ let(:query) do
8
+ <<~GRAPHQL
9
+ query OAuthClients {
10
+ oauthClients {
11
+ name
12
+ id
13
+ }
14
+ }
15
+ GRAPHQL
16
+ end
17
+
18
+ before do
19
+ create_list(:oauth_client, 2)
20
+ end
21
+
22
+ subject do
23
+ described_class.execute(
24
+ query,
25
+ variables: nil,
26
+ context: { scope: current_scope },
27
+ )
28
+ end
29
+
30
+ describe 'for an admin user' do
31
+ let(:current_scope) { :admin }
32
+
33
+ it 'returns Oauth Clients' do
34
+ expect(subject['errors']).to be_nil
35
+ expect(subject.dig('data', 'oauthClients').count).to eq(2)
36
+ end
37
+ end
38
+
39
+ describe 'for an email scoped user' do
40
+ let(:current_scope) { 'foo.com' }
41
+
42
+ it 'returns Oauth Clients' do
43
+ expect(subject['errors']).to be_nil
44
+ expect(subject.dig('data', 'oauthClients')).to be_nil
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # describe Osso::Models::AzureSamlProvider do
6
+ # subject { create(:azure_identity_provider) }
7
+
8
+ # describe '#saml_options' do
9
+ # it 'returns the required args' do
10
+ # expect(subject.saml_options).
11
+ # to match(
12
+ # domain: subject.domain,
13
+ # idp_cert: subject.idp_cert,
14
+ # idp_sso_target_url: subject.idp_sso_target_url,
15
+ # issuer: "id:#{subject.id}",
16
+ # )
17
+ # end
18
+ # end
19
+ # end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::Models::IdentityProvider do
6
+ subject { create(:okta_identity_provider) }
7
+
8
+ describe '#assertion_consumer_service_url' do
9
+ it 'returns the expected URI' do
10
+ ENV['BASE_URL'] = 'https://example.com'
11
+
12
+ expect(subject.assertion_consumer_service_url).to eq(
13
+ "https://example.com/auth/saml/#{subject.id}/callback",
14
+ )
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # describe Osso::Models::OktaSamlProvider do
6
+ # subject { create(:okta_identity_provider) }
7
+
8
+ # describe '#saml_options' do
9
+ # it 'returns the required args' do
10
+ # expect(subject.saml_options).
11
+ # to match(
12
+ # domain: subject.domain,
13
+ # idp_cert: subject.idp_cert,
14
+ # idp_sso_target_url: subject.idp_sso_target_url,
15
+ # issuer: subject.id,
16
+ # name_identifier_format: described_class::NAME_FORMAT,
17
+ # )
18
+ # end
19
+ # end
20
+ # end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::Admin do
6
+ let(:jwt_url) { 'https://foo.com/jwt' }
7
+ let(:jwt_hmac_secret) { SecureRandom.hex(32) }
8
+
9
+ before do
10
+ ENV['JWT_URL'] = jwt_url
11
+ ENV['JWT_HMAC_SECRET'] = jwt_hmac_secret
12
+ described_class.set(:views, spec_views)
13
+ end
14
+
15
+ describe 'get /admin' do
16
+ it 'redirects to JWT_URL without a session or token' do
17
+ get('/admin')
18
+
19
+ expect(last_response).to be_redirect
20
+ follow_redirect!
21
+ expect(last_request.url).to eq(jwt_url)
22
+ end
23
+
24
+ it 'redirects to JWT_URL with an invalid token' do
25
+ get('/admin', token: SecureRandom.hex(32))
26
+
27
+ expect(last_response).to be_redirect
28
+
29
+ follow_redirect!
30
+
31
+ expect(last_request.url).to eq(jwt_url)
32
+ end
33
+
34
+ it 'chomps the token and redirects to request path with valid token' do
35
+ token = JWT.encode(
36
+ { email: 'admin@saas.com', scope: 'admin' },
37
+ jwt_hmac_secret,
38
+ 'HS256',
39
+ )
40
+
41
+ get('/admin', { admin_token: token })
42
+
43
+ expect(last_response).to be_redirect
44
+ follow_redirect!
45
+ expect(last_request.url).to match('/admin')
46
+ end
47
+
48
+ it 'renders the admin page for a valid session token' do
49
+ token = JWT.encode(
50
+ { email: 'admin@saas.com', scope: 'admin' },
51
+ jwt_hmac_secret,
52
+ 'HS256',
53
+ )
54
+
55
+ get('/admin', {}, 'rack.session' => { admin_token: token })
56
+
57
+ expect(last_response).to be_ok
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'App' do
6
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::Auth do
6
+ describe 'post /auth/saml/:uuid/callback' do
7
+ describe 'for an Okta SAML provider' do
8
+ let(:enterprise) { create(:enterprise_with_okta) }
9
+ let(:okta_provider) { enterprise.identity_providers.first }
10
+
11
+ describe "on the user's first authentication" do
12
+ it 'creates a user' do
13
+ mock_saml_omniauth
14
+
15
+ expect do
16
+ post(
17
+ "/auth/saml/#{okta_provider.id}/callback",
18
+ nil,
19
+ {
20
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
21
+ 'identity_provider' => okta_provider,
22
+ },
23
+ )
24
+ end.to change { Osso::Models::User.count }.by(1)
25
+ end
26
+
27
+ it 'creates an authorization_code' do
28
+ mock_saml_omniauth
29
+
30
+ expect do
31
+ post(
32
+ "/auth/saml/#{okta_provider.id}/callback",
33
+ nil,
34
+ {
35
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
36
+ 'identity_provider' => okta_provider,
37
+ },
38
+ )
39
+ end.to change { Osso::Models::AuthorizationCode.count }.by(1)
40
+ end
41
+ end
42
+
43
+ describe 'on subsequent authentications' do
44
+ let!(:enterprise) { create(:enterprise_with_okta) }
45
+ let!(:okta_provider) { enterprise.identity_providers.first }
46
+ let(:user) { create(:user, identity_provider_id: okta_provider.id) }
47
+
48
+ before do
49
+ mock_saml_omniauth(email: user.email, id: user.idp_id)
50
+ end
51
+
52
+ it 'creates a user' do
53
+ expect do
54
+ post(
55
+ "/auth/saml/#{okta_provider.id}/callback",
56
+ nil,
57
+ {
58
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
59
+ 'identity_provider' => okta_provider,
60
+ },
61
+ )
62
+ end.to_not(change { Osso::Models::User.count })
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'for an (Azure) ADFS SAML provider' do
68
+ let(:enterprise) { create(:enterprise_with_azure) }
69
+ let(:azure_provider) { enterprise.provider }
70
+
71
+ describe "on the user's first authentication" do
72
+ it 'creates a user' do
73
+ mock_saml_omniauth
74
+
75
+ expect do
76
+ post(
77
+ "/auth/saml/#{azure_provider.id}/callback",
78
+ nil,
79
+ {
80
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
81
+ 'identity_provider' => azure_provider,
82
+ },
83
+ )
84
+ end.to change { Osso::Models::User.count }.by(1)
85
+ end
86
+ end
87
+
88
+ describe 'on subsequent authentications' do
89
+ let!(:enterprise) { create(:enterprise_with_azure) }
90
+ let!(:azure_provider) { enterprise.provider }
91
+ let(:user) { create(:user, identity_provider_id: azure_provider.id) }
92
+
93
+ before do
94
+ mock_saml_omniauth(email: user.email, id: user.idp_id)
95
+ end
96
+
97
+ it 'creates a user' do
98
+ expect do
99
+ post(
100
+ "/auth/saml/#{azure_provider.id}/callback",
101
+ nil,
102
+ {
103
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
104
+ 'identity_provider' => azure_provider,
105
+ },
106
+ )
107
+ end.to_not(change { Osso::Models::User.count })
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::Oauth do
6
+ let(:client) { create(:oauth_client) }
7
+
8
+ describe 'get /oauth/authorize' do
9
+ describe 'with a valid client ID and redirect URI' do
10
+ describe 'for a domain that does not belong to an enterprise' do
11
+ it '404s' do
12
+ create(:enterprise_with_okta, domain: 'foo.com')
13
+
14
+ get(
15
+ '/oauth/authorize',
16
+ domain: 'bar.org',
17
+ client_id: client.identifier,
18
+ response_type: 'code',
19
+ redirect_uri: client.redirect_uri_values.sample,
20
+ )
21
+
22
+ expect(last_response.status).to eq(404)
23
+ end
24
+ end
25
+
26
+ describe 'for an enterprise domain with one SAML provider' do
27
+ it 'redirects to /auth/saml/:provider_id' do
28
+ enterprise = create(:enterprise_with_okta, oauth_client: client)
29
+
30
+ get(
31
+ '/oauth/authorize',
32
+ domain: enterprise.domain,
33
+ client_id: client.identifier,
34
+ response_type: 'code',
35
+ redirect_uri: client.redirect_uri_values.sample,
36
+ )
37
+
38
+ provider_id = enterprise.identity_providers.first.id
39
+
40
+ expect(last_response).to be_redirect
41
+ follow_redirect!
42
+ expect(last_request.url).to match("auth/saml/#{provider_id}")
43
+ end
44
+ end
45
+
46
+ describe 'for an enterprise domain with multiple SAML providers' do
47
+ it 'renders the multiple providers screen' do
48
+ enterprise = create(:enterprise_with_multiple_providers)
49
+
50
+ get(
51
+ '/oauth/authorize',
52
+ domain: enterprise.domain,
53
+ client_id: client.identifier,
54
+ response_type: 'code',
55
+ redirect_uri: client.redirect_uri_values.sample,
56
+ )
57
+
58
+ expect(last_response).to be_ok
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ describe 'post /oauth/token' do
65
+ describe 'with a valid unexpired code, client secret, client ID and redirect URI' do
66
+ it 'returns an access token' do
67
+ user = create(:user)
68
+ code = user.authorization_codes.valid.first
69
+
70
+ post(
71
+ '/oauth/token',
72
+ client_id: client.identifier,
73
+ client_secret: client.secret,
74
+ grant_type: 'authorization_code',
75
+ redirect_uri: code.redirect_uri,
76
+ code: code.token,
77
+ )
78
+
79
+ expect(last_response.status).to eq(200)
80
+ expect(last_json_response.to_h).to match(
81
+ access_token: a_string_matching(/\w{64}/),
82
+ expires_in: an_instance_of(Integer),
83
+ token_type: 'bearer',
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ describe 'get /oauth/me' do
90
+ describe 'with a valid unexpired access token' do
91
+ it 'returns the user' do
92
+ user = create(:user)
93
+ code = user.authorization_codes.valid.first
94
+
95
+ get(
96
+ '/oauth/me',
97
+ access_token: code.access_token.to_bearer_token,
98
+ )
99
+
100
+ expect(last_response.status).to eq(200)
101
+ expect(last_json_response).to eq(
102
+ email: user.email,
103
+ id: user.id,
104
+ idp: 'Okta',
105
+ )
106
+ end
107
+ end
108
+
109
+ describe 'with an expired access token' do
110
+ it 'returns 404' do
111
+ code = create(:authorization_code)
112
+ code.access_token.expired!
113
+
114
+ get(
115
+ '/oauth/me',
116
+ access_token: code.access_token.to_bearer_token,
117
+ )
118
+
119
+ expect(last_response.status).to eq(404)
120
+ end
121
+ end
122
+
123
+ describe 'with an invalid access token' do
124
+ it 'returns 404' do
125
+ get(
126
+ '/oauth/me',
127
+ access_token: SecureRandom.hex(32),
128
+ )
129
+
130
+ expect(last_response.status).to eq(404)
131
+ end
132
+ end
133
+ end
134
+ end