osso 0.0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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