osso 0.0.5.pre.delta → 0.0.5.pre.epsilon

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9762b3c9f7bae7bbb066f2ad4a80cc6ee8b360a730781983712d2b61465e2de2
4
- data.tar.gz: 498ad481465fece9cbe1a2045972cfb0e7719ce91e00446085dd88a5eca3b045
3
+ metadata.gz: 82bd613a1b8d5eb4f9c549d9d855c629698a120d2ee99bf3a52f6911fa4d080a
4
+ data.tar.gz: a67aec622f76bcc42cc16065d11a795c09893caa1115759b5739fa384108f6f2
5
5
  SHA512:
6
- metadata.gz: b59eb8d8e3008fe05db242994a1e58b7698b72d4ae657c54fe5a8a3e6d8d6db54b69bbefcef4480c7b901ef9a3b7bb3abed575fbf2f76fe9ba5e52901cd29886
7
- data.tar.gz: b54fa57263e4b675ec4a8feb2d51e8266621663ca4036b61b114aeae4134a6d089ed99ebbc82c75caf44968b082e1e37be4cc74cbd3c6ffa72cd1f75938f4f91
6
+ metadata.gz: c2a1ba5df34d2e175ed7a3ab7e61dcb573b5eb9a636753f7159860f817e6b61515b9350b63a1ac1ffece994f27a7f17d61d2de05e5f6a0468168cfb7a268f879
7
+ data.tar.gz: 475eba77f824e32701ff9eae95bef4663f2e0de8d43c9d542846f6ecaec55edb1242251cb4ed50b9cb1db627252293cf406a633cc8c0c63fb7f419e10dc95e3c
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- osso (0.0.5.pre.gamma)
4
+ osso (0.0.5.pre.epsilon)
5
5
  activesupport (>= 6.0.3.2)
6
6
  graphql
7
7
  jwt
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
+ require_relative 'osso/error/error'
4
5
  require_relative 'osso/helpers/helpers'
5
6
  require_relative 'osso/lib/app_config'
6
7
  require_relative 'osso/lib/oauth2_token'
7
8
  require_relative 'osso/lib/route_map'
9
+ require_relative 'osso/lib/saml_handler'
8
10
  require_relative 'osso/models/models'
9
11
  require_relative 'osso/routes/routes'
10
12
  require_relative 'osso/graphql/schema'
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ end
6
+ end
7
+
8
+ require_relative 'missing_saml_attribute_error'
9
+ require_relative 'oauth_error'
10
+ require_relative 'saml_config_error'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class MissingSamlAttributeError < StandardError; end
6
+
7
+ class MissingSamlEmailAttributeError < MissingSamlAttributeError
8
+ def message
9
+ 'SAML response does not include the attribute `email`. ' \
10
+ "Review the setup guide and check the attributes you're sending from your Identity Provider."
11
+ end
12
+ end
13
+
14
+ class MissingSamlIdAttributeError < MissingSamlAttributeError
15
+ def message
16
+ 'SAML response does not include the attribute `id` or `idp_id`.' \
17
+ "Review the setup guide and check the attributes you're sending from your Identity Provider."
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class OAuthError < StandardError; end
6
+
7
+ class NoAccountForOAuthClientError < OAuthError
8
+ def message
9
+ 'No customer account exists for the requested domain and OAuth client pair.' \
10
+ "Review our OAuth documentation, and check you're using the correct OAuth client identifier"
11
+ end
12
+ end
13
+
14
+ class InvalidOAuthClientIdentifier < MissingSamlAttributeError
15
+ def message
16
+ 'No OAuth client exists for the requested OAuth client identifier.' \
17
+ "Review our OAuth documentation, and check you're using the correct OAuth client identifier"
18
+ end
19
+ end
20
+
21
+ class InvalidRedirectUri < MissingSamlAttributeError
22
+ def message
23
+ 'Invalid Redirect URI for the requested OAuth client identifier.' \
24
+ "Review our OAuth documentation, check you're using the correct OAuth client identifier " \
25
+ 'and confirm your Redirect URI allow list.'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class SamlConfigError < StandardError; end
6
+
7
+ class InvalidACSURLError < SamlConfigError
8
+ def message
9
+ 'The ACS URL specfied in your Identity Provider configuration is malformed.'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ class SamlHandler
5
+ attr_accessor :session, :provider, :attributes
6
+
7
+ def self.perform(**attrs)
8
+ new(attrs).perform
9
+ end
10
+
11
+ def initialize(auth_hash:, provider_id:, session:)
12
+ find_provider(provider_id)
13
+ @attributes = auth_hash&.extra&.response_object&.attributes
14
+ @session = session
15
+ end
16
+
17
+ def perform
18
+ validate_attributes
19
+ provider.active!
20
+ redirect_uri
21
+ end
22
+
23
+ private
24
+
25
+ def find_provider(id)
26
+ @provider ||= Models::IdentityProvider.find(id)
27
+ rescue ActiveRecord::RecordNotFound
28
+ raise Osso::Error::InvalidACSURLError
29
+ end
30
+
31
+ def validate_attributes
32
+ raise Osso::Error::MissingSamlIdAttributeError unless id_attribute
33
+ raise Osso::Error::MissingSamlEmailAttributeError unless email_attribute
34
+ end
35
+
36
+ def id_attribute
37
+ @id_attribute ||= attributes[:id] || attributes[:idp_id]
38
+ end
39
+
40
+ def email_attribute
41
+ attributes[:email]
42
+ end
43
+
44
+ def user
45
+ @user ||= Models::User.where(
46
+ email: email_attribute,
47
+ idp_id: id_attribute,
48
+ ).first_or_create! do |new_user|
49
+ new_user.enterprise_account_id = provider.enterprise_account_id
50
+ new_user.identity_provider_id = provider.id
51
+ end
52
+ end
53
+
54
+ def authorization_code
55
+ @authorization_code ||= user.authorization_codes.create!(
56
+ oauth_client: provider.oauth_client,
57
+ redirect_uri: redirect_uri_base,
58
+ )
59
+ end
60
+
61
+ def redirect_uri
62
+ redirect_uri_base + redirect_uri_querystring
63
+ end
64
+
65
+ def redirect_uri_base
66
+ return provider.oauth_client.primary_redirect_uri.uri if valid_idp_initiated_flow
67
+
68
+ session[:osso_oauth_redirect_uri]
69
+ end
70
+
71
+ def redirect_uri_querystring
72
+ "?code=#{CGI.escape(authorization_code.token)}&state=#{provider_state}"
73
+ end
74
+
75
+ def provider_state
76
+ return 'IDP_INITIATED' if valid_idp_initiated_flow
77
+
78
+ session.delete(:osso_oauth_state)
79
+ end
80
+
81
+ def valid_idp_initiated_flow
82
+ !session[:osso_oauth_redirect_uri] && !session[:osso_oauth_state]
83
+ end
84
+ end
85
+ end
@@ -27,9 +27,16 @@ module Osso
27
27
  end
28
28
  end
29
29
 
30
- namespace '/auth' do # rubocop:disable Metrics/BlockLength
30
+ OmniAuth.config.on_failure = proc do |env|
31
+ OmniAuth::FailureEndpoint.new(env).redirect_to_failure
32
+ end
33
+
34
+ error do
35
+ erb :error
36
+ end
37
+
38
+ namespace '/auth' do
31
39
  get '/failure' do
32
- @error = params[:message]
33
40
  erb :error
34
41
  end
35
42
  # Enterprise users are sent here after authenticating against
@@ -37,47 +44,17 @@ module Osso
37
44
  # and then create an authorization code for that user. The user
38
45
  # is redirected back to your application with this code
39
46
  # as a URL query param, which you then exchange for an access token.
40
- post '/saml/:id/callback' do
41
- provider = Models::IdentityProvider.find(params[:id])
42
- @oauth_client = provider.oauth_client
43
-
44
- # TODO: PORC for validating attributes
45
- attributes = env['omniauth.auth']&.
46
- extra&.
47
- response_object&.
48
- attributes
49
-
50
- user = Models::User.where(
51
- email: attributes[:email],
52
- idp_id: attributes[:id] || attributes[:idp_id],
53
- ).first_or_create! do |new_user|
54
- new_user.enterprise_account_id = provider.enterprise_account_id
55
- new_user.identity_provider_id = provider.id
56
- end
57
-
58
- authorization_code = user.authorization_codes.create!(
59
- oauth_client: @oauth_client,
60
- redirect_uri: redirect_uri,
47
+ post '/saml/:provider_id/callback' do
48
+ redirect_uri = SamlHandler.perform(
49
+ auth_hash: env['omniauth.auth'],
50
+ provider_id: params[:provider_id],
51
+ session: session,
61
52
  )
62
- provider.active!
63
-
64
- redirect(redirect_uri + "?code=#{CGI.escape(authorization_code.token)}&state=#{provider_state}")
65
- end
66
-
67
- def redirect_uri
68
- return @oauth_client.primary_redirect_uri.uri if valid_idp_initiated_flow
69
-
70
- session[:osso_oauth_redirect_uri]
71
- end
72
53
 
73
- def provider_state
74
- return @provider_state = 'IDP_INITIATED' if valid_idp_initiated_flow
75
-
76
- session.delete(:osso_oauth_state)
77
- end
78
-
79
- def valid_idp_initiated_flow
80
- !session[:osso_oauth_redirect_uri] && !session[:osso_oauth_state]
54
+ redirect(redirect_uri)
55
+ rescue Osso::Error::InvalidACSURLError => e
56
+ @error = e
57
+ erb :error
81
58
  end
82
59
  end
83
60
  end
@@ -16,28 +16,18 @@ module Osso
16
16
  # Once they complete IdP login, they will be returned to the
17
17
  # redirect_uri with an authorization code parameter.
18
18
  get '/authorize' do
19
- Rack::OAuth2::Server::Authorize.new do |req, _res|
20
- client = Models::OauthClient.find_by!(identifier: req.client_id)
21
- session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
22
- session[:osso_oauth_state] = params[:state]
23
- end.call(env)
19
+ client = find_client(params[:client_id])
20
+ enterprise = find_account(domain: params[:domain], client_id: client.id)
24
21
 
25
- enterprise = Models::EnterpriseAccount.
26
- includes(:identity_providers).
27
- find_by!(domain: params[:domain])
22
+ validate_oauth_request(env)
28
23
 
29
24
  redirect "/auth/saml/#{enterprise.provider.id}" if enterprise.single_provider?
30
25
 
31
26
  @providers = enterprise.identity_providers
32
27
  erb :multiple_providers
33
28
 
34
- rescue Rack::OAuth2::Server::Authorize::BadRequest => e
35
- @error = e
36
- erb :error
37
- rescue ActiveRecord::RecordNotFound => e
29
+ rescue Osso::Error::OAuthError => e
38
30
  @error = e
39
- @error = 'No OAuth Client exists for the provided client_id' if e.model == 'Osso::Models::OauthClient'
40
- @error = "No Customer exists with the domain #{params[:domain]}" if e.model == 'Osso::Models::EnterpriseAccount'
41
31
  erb :error
42
32
  end
43
33
 
@@ -66,5 +56,31 @@ module Osso
66
56
  user
67
57
  end
68
58
  end
59
+
60
+ private
61
+
62
+ def find_account(domain:, client_id:)
63
+ Models::EnterpriseAccount.
64
+ includes(:identity_providers).
65
+ find_by!(domain: domain, oauth_client_id: client_id)
66
+ rescue ActiveRecord::RecordNotFound
67
+ raise Osso::Error::NoAccountForOAuthClientError
68
+ end
69
+
70
+ def find_client(identifier)
71
+ @client ||= Models::OauthClient.find_by!(identifier: identifier)
72
+ rescue ActiveRecord::RecordNotFound
73
+ raise Osso::Error::InvalidOAuthClientIdentifier
74
+ end
75
+
76
+ def validate_oauth_request(env)
77
+ Rack::OAuth2::Server::Authorize.new do |req, _res|
78
+ client = find_client(req[:client_id])
79
+ session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
80
+ session[:osso_oauth_state] = params[:state]
81
+ end.call(env)
82
+ rescue Rack::OAuth2::Server::Authorize::BadRequest
83
+ raise Osso::Error::InvalidRedirectUri
84
+ end
69
85
  end
70
86
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
- VERSION = '0.0.5-delta'
4
+ VERSION = '0.0.5-epsilon'
5
5
  end
@@ -43,7 +43,6 @@ describe Osso::Auth do
43
43
  nil,
44
44
  {
45
45
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
46
- 'identity_provider' => okta_provider,
47
46
  },
48
47
  )
49
48
  end.to change { Osso::Models::User.count }.by(1)
@@ -58,7 +57,6 @@ describe Osso::Auth do
58
57
  nil,
59
58
  {
60
59
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
61
- 'identity_provider' => okta_provider,
62
60
  },
63
61
  )
64
62
  end.to change { Osso::Models::AuthorizationCode.count }.by(1)
@@ -73,7 +71,6 @@ describe Osso::Auth do
73
71
  nil,
74
72
  {
75
73
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
76
- 'identity_provider' => okta_provider,
77
74
  },
78
75
  )
79
76
  expect(last_response).to be_redirect
@@ -99,7 +96,6 @@ describe Osso::Auth do
99
96
  nil,
100
97
  {
101
98
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
102
- 'identity_provider' => okta_provider,
103
99
  },
104
100
  )
105
101
  end.to_not(change { Osso::Models::User.count })
@@ -110,7 +106,6 @@ describe Osso::Auth do
110
106
  nil,
111
107
  {
112
108
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
113
- 'identity_provider' => okta_provider,
114
109
  },
115
110
  )
116
111
  expect(okta_provider.reload.status).to eq('ACTIVE')
@@ -132,7 +127,6 @@ describe Osso::Auth do
132
127
  nil,
133
128
  {
134
129
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
135
- 'identity_provider' => azure_provider,
136
130
  },
137
131
  )
138
132
  end.to change { Osso::Models::User.count }.by(1)
@@ -146,7 +140,6 @@ describe Osso::Auth do
146
140
  nil,
147
141
  {
148
142
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
149
- 'identity_provider' => azure_provider,
150
143
  },
151
144
  )
152
145
 
@@ -170,7 +163,6 @@ describe Osso::Auth do
170
163
  nil,
171
164
  {
172
165
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
173
- 'identity_provider' => azure_provider,
174
166
  },
175
167
  )
176
168
  end.to_not(change { Osso::Models::User.count })
@@ -178,4 +170,39 @@ describe Osso::Auth do
178
170
  end
179
171
  end
180
172
  end
173
+
174
+ context 'with an invalid SAML response' do
175
+ describe 'post /auth/saml/:uuid/callback' do
176
+ let!(:enterprise) { create(:enterprise_with_azure) }
177
+ let!(:azure_provider) { enterprise.provider }
178
+
179
+ it 'raises an error when email is missing' do
180
+ mock_saml_omniauth(email: nil, id: SecureRandom.uuid)
181
+
182
+ expect do
183
+ post(
184
+ "/auth/saml/#{azure_provider.id}/callback",
185
+ nil,
186
+ {
187
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
188
+ },
189
+ )
190
+ end.to raise_error(Osso::Error::MissingSamlEmailAttributeError)
191
+ end
192
+
193
+ it 'raises an error when id is missing' do
194
+ mock_saml_omniauth(email: Faker::Internet.email, id: nil)
195
+
196
+ expect do
197
+ post(
198
+ "/auth/saml/#{azure_provider.id}/callback",
199
+ nil,
200
+ {
201
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
202
+ },
203
+ )
204
+ end.to raise_error(Osso::Error::MissingSamlIdAttributeError)
205
+ end
206
+ end
207
+ end
181
208
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: osso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5.pre.delta
4
+ version: 0.0.5.pre.epsilon
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Bauch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-18 00:00:00.000000000 Z
11
+ date: 2020-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -273,6 +273,10 @@ files:
273
273
  - lib/osso/db/migrate/20200826201852_create_app_config.rb
274
274
  - lib/osso/db/migrate/20200913154919_add_one_login_to_identity_provider_service_enum.rb
275
275
  - lib/osso/db/migrate/20200916125543_add_google_to_identity_provider_service_enum.rb
276
+ - lib/osso/error/error.rb
277
+ - lib/osso/error/missing_saml_attribute_error.rb
278
+ - lib/osso/error/oauth_error.rb
279
+ - lib/osso/error/saml_config_error.rb
276
280
  - lib/osso/graphql/.DS_Store
277
281
  - lib/osso/graphql/mutation.rb
278
282
  - lib/osso/graphql/mutations.rb
@@ -313,6 +317,7 @@ files:
313
317
  - lib/osso/lib/app_config.rb
314
318
  - lib/osso/lib/oauth2_token.rb
315
319
  - lib/osso/lib/route_map.rb
320
+ - lib/osso/lib/saml_handler.rb
316
321
  - lib/osso/models/access_token.rb
317
322
  - lib/osso/models/app_config.rb
318
323
  - lib/osso/models/authorization_code.rb
@@ -347,6 +352,7 @@ files:
347
352
  - spec/graphql/query/identity_provider_spec.rb
348
353
  - spec/graphql/query/oauth_clients_spec.rb
349
354
  - spec/helpers/auth_spec.rb
355
+ - spec/lib/saml_handler_spec.rb
350
356
  - spec/models/identity_provider_spec.rb
351
357
  - spec/routes/admin_spec.rb
352
358
  - spec/routes/app_spec.rb