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

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