osso 0.0.5.pre.gamma → 0.0.5.pre.theta

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: f8349ed4f80afe46d642e3e1e7aaa9b9d0f90ece6e0fd6b8e0db416f74cc54f1
4
- data.tar.gz: 9aa776d8e26b04570e4123baf232404c3e9059c66bcb1fe39f2ac337c59e7edd
3
+ metadata.gz: 80defe1af30691a237f560c7cbdaed6ad25dddc62aaa55c42e317ce6fb47e5f6
4
+ data.tar.gz: bf4205e1e2bfb2cc50af764e094a504a78753427669efe78f7414a67f176156b
5
5
  SHA512:
6
- metadata.gz: aa38f696f541aa36893252c26aa1ffea8b6ac33b05b3bb130ecf7b1b71094042a7546984d67857ba8002c47744c47a1c813e7d972f38068be6c50f35a11aa9b7
7
- data.tar.gz: 515be1591f3d5ce752057483cfa8e7d94d1665a7534fe1112c5c607f7ec2417ff650367826d7eec71d64086d735994c4f1b7ac322756df103adbdb6c4958bec2
6
+ metadata.gz: 1b13b109aafee1913e18fc9acaf64f7876a0468ab7477c4666bf002fcde298edd3d2b72e58121998e2a7ddd93b54673e0739781f5ec57c56423369223d5550a5
7
+ data.tar.gz: 2d37d7a3fb5a8d19feb6efb3a5c45e84023b8abcb5448b0ba884dddb05112dc06f5a5d91773587e80b6530b7c94b03eef03cf96020f07ad84824b273c00c4af9
@@ -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.eta)
5
5
  activesupport (>= 6.0.3.2)
6
6
  graphql
7
7
  jwt
@@ -18,12 +18,12 @@ PATH
18
18
  GEM
19
19
  remote: https://rubygems.org/
20
20
  specs:
21
- activemodel (6.0.3.2)
22
- activesupport (= 6.0.3.2)
23
- activerecord (6.0.3.2)
24
- activemodel (= 6.0.3.2)
25
- activesupport (= 6.0.3.2)
26
- activesupport (6.0.3.2)
21
+ activemodel (6.0.3.3)
22
+ activesupport (= 6.0.3.3)
23
+ activerecord (6.0.3.3)
24
+ activemodel (= 6.0.3.3)
25
+ activesupport (= 6.0.3.3)
26
+ activesupport (6.0.3.3)
27
27
  concurrent-ruby (~> 1.0, >= 1.0.2)
28
28
  i18n (>= 0.7, < 2)
29
29
  minitest (~> 5.1)
@@ -39,24 +39,23 @@ GEM
39
39
  attr_required (1.0.1)
40
40
  bindata (2.4.8)
41
41
  coderay (1.1.3)
42
- concurrent-ruby (1.1.6)
43
- crack (0.4.3)
44
- safe_yaml (~> 1.0.0)
42
+ concurrent-ruby (1.1.7)
43
+ crack (0.4.4)
45
44
  database_cleaner (1.8.5)
46
45
  database_cleaner-active_record (1.8.0)
47
46
  activerecord
48
47
  database_cleaner (~> 1.8.0)
49
48
  diff-lcs (1.4.4)
50
49
  docile (1.3.2)
51
- factory_bot (6.0.2)
50
+ factory_bot (6.1.0)
52
51
  activesupport (>= 5.0.0)
53
- faker (2.13.0)
52
+ faker (2.14.0)
54
53
  i18n (>= 1.6, < 2)
55
54
  graphql (1.11.4)
56
55
  hashdiff (1.0.1)
57
56
  hashie (4.1.0)
58
57
  httpclient (2.8.3)
59
- i18n (1.8.3)
58
+ i18n (1.8.5)
60
59
  concurrent-ruby (~> 1.0)
61
60
  json (2.3.1)
62
61
  json-jwt (1.13.0)
@@ -66,7 +65,7 @@ GEM
66
65
  jwt (2.2.2)
67
66
  method_source (1.0.0)
68
67
  mini_portile2 (2.4.0)
69
- minitest (5.14.1)
68
+ minitest (5.14.2)
70
69
  multi_json (1.15.0)
71
70
  mustermann (1.1.1)
72
71
  ruby2_keywords (~> 0.0.1)
@@ -87,7 +86,7 @@ GEM
87
86
  pry (0.13.1)
88
87
  coderay (~> 1.1)
89
88
  method_source (~> 1.0)
90
- public_suffix (4.0.5)
89
+ public_suffix (4.0.6)
91
90
  rack (2.2.3)
92
91
  rack-contrib (2.2.0)
93
92
  rack (~> 2.0)
@@ -103,7 +102,7 @@ GEM
103
102
  rack (>= 1.0, < 3)
104
103
  rainbow (3.0.0)
105
104
  rake (13.0.1)
106
- regexp_parser (1.7.1)
105
+ regexp_parser (1.8.0)
107
106
  rexml (3.2.4)
108
107
  rspec (3.9.0)
109
108
  rspec-core (~> 3.9.0)
@@ -118,22 +117,21 @@ GEM
118
117
  diff-lcs (>= 1.2.0, < 2.0)
119
118
  rspec-support (~> 3.9.0)
120
119
  rspec-support (3.9.3)
121
- rubocop (0.86.0)
120
+ rubocop (0.91.0)
122
121
  parallel (~> 1.10)
123
- parser (>= 2.7.0.1)
122
+ parser (>= 2.7.1.1)
124
123
  rainbow (>= 2.2.2, < 4.0)
125
124
  regexp_parser (>= 1.7)
126
125
  rexml
127
- rubocop-ast (>= 0.0.3, < 1.0)
126
+ rubocop-ast (>= 0.4.0, < 1.0)
128
127
  ruby-progressbar (~> 1.7)
129
128
  unicode-display_width (>= 1.4.0, < 2.0)
130
- rubocop-ast (0.1.0)
131
- parser (>= 2.7.0.1)
129
+ rubocop-ast (0.4.2)
130
+ parser (>= 2.7.1.4)
132
131
  ruby-progressbar (1.10.1)
133
132
  ruby-saml (1.11.0)
134
133
  nokogiri (>= 1.5.10)
135
134
  ruby2_keywords (0.0.2)
136
- safe_yaml (1.0.5)
137
135
  simplecov (0.17.0)
138
136
  docile (~> 1.1)
139
137
  json (>= 1.8, < 3)
@@ -158,11 +156,11 @@ GEM
158
156
  tzinfo (1.2.7)
159
157
  thread_safe (~> 0.1)
160
158
  unicode-display_width (1.7.0)
161
- webmock (3.8.3)
159
+ webmock (3.9.1)
162
160
  addressable (>= 2.3.6)
163
161
  crack (>= 0.3.2)
164
162
  hashdiff (>= 0.4.0, < 2.0.0)
165
- zeitwerk (2.3.1)
163
+ zeitwerk (2.4.0)
166
164
 
167
165
  PLATFORMS
168
166
  ruby
@@ -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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class AccountConfigurationError < Base; end
6
+
7
+ class MissingConfiguredIdentityProvider < AccountConfigurationError
8
+ def initialize(domain: 'The requested domain')
9
+ @domain = domain
10
+ end
11
+
12
+ def message
13
+ "#{@domain} has no configured Identity Provider. " \
14
+ 'SAML configuartion must be finalized before a user ' \
15
+ 'for this domain can sign in with SSO.'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class Base < StandardError
6
+ def docs_url
7
+ nil
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative 'account_configuration_error'
14
+ require_relative 'missing_saml_attribute_error'
15
+ require_relative 'oauth_error'
16
+ require_relative 'saml_config_error'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class MissingSamlAttributeError < Base; 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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class OAuthError < Base
6
+ def docs_url
7
+ 'https://ossoapp.com/docs/integration/oauth-consumption'
8
+ end
9
+ end
10
+
11
+ class NoAccountForOAuthClientError < OAuthError
12
+ def initialize(domain: 'the requested domain')
13
+ @domain = domain
14
+ end
15
+
16
+ def message
17
+ "No customer account exists for #{@domain} and OAuth client pair. " \
18
+ "Review our OAuth documentation, and check you're using the correct OAuth client identifier. " \
19
+ 'This usually suggests an engineering issue with your ENV variables.'
20
+ end
21
+ end
22
+
23
+ class InvalidOAuthClientIdentifier < OAuthError
24
+ def message
25
+ 'No OAuth client exists for the requested OAuth client identifier. ' \
26
+ "Review our OAuth documentation, and ensure you're using a valid OAuth client identifier. " \
27
+ 'OAuth credentials may have been regenerated by your team.'
28
+ end
29
+ end
30
+
31
+ class InvalidRedirectUri < OAuthError
32
+ def initialize(redirect_uri:)
33
+ @redirect_uri = redirect_uri
34
+ end
35
+
36
+ def message
37
+ "The requested redirect URI #{@redirect_uri} is not on the allow-list for the rquested OAuth client identifier. " \
38
+ "Review our OAuth documentation, check you're using the correct OAuth client identifier, " \
39
+ 'and confirm your Redirect URI allow-list includes the appropriate URI(s).'
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Error
5
+ class SamlConfigError < Base
6
+ def message
7
+ 'Something went wrong with your SAML configuration.'
8
+ end
9
+ end
10
+
11
+ class InvalidACSURLError < SamlConfigError
12
+ def message
13
+ 'The ACS URL specfied in your Identity Provider configuration is malformed.'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -16,6 +16,7 @@ module Osso
16
16
  field :sso_url, String, null: true
17
17
  field :sso_cert, String, null: true
18
18
  field :status, Types::IdentityProviderStatus, null: false
19
+ field :acs_url_validator, String, null: false
19
20
  end
20
21
  end
21
22
  end
@@ -4,10 +4,10 @@ module Osso
4
4
  module GraphQL
5
5
  module Types
6
6
  class IdentityProviderStatus < BaseEnum
7
- value('Pending', value: 'PENDING')
8
- value('Configured', value: 'CONFIGURED')
9
- value('Active', value: 'ACTIVE')
10
- value('Error', value: 'ERROR')
7
+ value('Pending', value: 'pending')
8
+ value('Configured', value: 'configured')
9
+ value('Active', value: 'active')
10
+ value('Error', value: 'error')
11
11
  end
12
12
  end
13
13
  end
@@ -66,7 +66,7 @@ module Osso
66
66
  end
67
67
 
68
68
  def token
69
- request.env['admin_token'] || session['admin_token'] || request['admin_token']
69
+ session['admin_token'] || request.env['HTTP_AUTHORIZATION'] || request.params['admin_token']
70
70
  end
71
71
 
72
72
  def chomp_token
@@ -6,6 +6,7 @@ module Osso
6
6
  module RouteMap
7
7
  def self.included(klass)
8
8
  klass.class_eval do
9
+
9
10
  use Osso::Admin
10
11
  use Osso::Auth
11
12
  use Osso::Oauth
@@ -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
@@ -13,7 +13,7 @@ module Osso
13
13
  has_many :identity_providers
14
14
 
15
15
  def single_provider?
16
- identity_providers.one?
16
+ identity_providers.not_pending.one?
17
17
  end
18
18
 
19
19
  def provider
@@ -10,6 +10,8 @@ module Osso
10
10
  before_save :set_status
11
11
  validate :sso_cert_valid
12
12
 
13
+ enum status: { pending: "PENDING", configured: 'CONFIGURED', active: "ACTIVE", error: "ERROR"}
14
+
13
15
  PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"
14
16
  PEM_FOOTER = "\n-----END CERTIFICATE-----"
15
17
 
@@ -38,18 +40,20 @@ module Osso
38
40
 
39
41
  alias acs_url assertion_consumer_service_url
40
42
 
41
- def set_status
42
- return if status != 'PENDING'
43
+ def acs_url_validator
44
+ Regexp.escape(acs_url)
45
+ end
43
46
 
44
- self.status = 'CONFIGURED' if sso_url && sso_cert
47
+ def set_status
48
+ self.status = 'configured' if sso_url && sso_cert && pending?
45
49
  end
46
50
 
47
51
  def active!
48
- update(status: 'ACTIVE')
52
+ update(status: 'active')
49
53
  end
50
54
 
51
55
  def error!
52
- update(status: 'ERROR')
56
+ update(status: 'error')
53
57
  end
54
58
 
55
59
  def root_url
@@ -16,37 +16,37 @@ module Osso
16
16
  get '/login' do
17
17
  token_protected!
18
18
 
19
- erb :admin
19
+ erb :admin, layout: false
20
20
  end
21
21
 
22
22
  get '' do
23
23
  internal_protected!
24
24
 
25
- erb :admin
25
+ erb :admin, layout: false
26
26
  end
27
27
 
28
28
  get '/enterprise' do
29
29
  token_protected!
30
30
 
31
- erb :admin
31
+ erb :admin, layout: false
32
32
  end
33
33
 
34
34
  get '/enterprise/:domain' do
35
35
  enterprise_protected!(params[:domain])
36
36
 
37
- erb :admin
37
+ erb :admin, layout: false
38
38
  end
39
39
 
40
40
  get '/config' do
41
41
  admin_protected!
42
42
 
43
- erb :admin
43
+ erb :admin, layout: false
44
44
  end
45
45
 
46
46
  get '/config/:id' do
47
47
  admin_protected!
48
48
 
49
- erb :admin
49
+ erb :admin, layout: false
50
50
  end
51
51
  end
52
52
  end
@@ -13,7 +13,7 @@ module Osso
13
13
  UUID_REGEXP =
14
14
  /[0-9a-f]{8}-[0-9a-f]{3,4}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{12}/.
15
15
  freeze
16
-
16
+
17
17
  use OmniAuth::Builder do
18
18
  OmniAuth::MultiProvider.register(
19
19
  self,
@@ -22,14 +22,39 @@ module Osso
22
22
  path_prefix: '/auth/saml',
23
23
  callback_suffix: 'callback',
24
24
  ) do |identity_provider_id, _env|
25
- Models::IdentityProvider.find(identity_provider_id).
25
+ Models::IdentityProvider.
26
+ not_pending.
27
+ find(identity_provider_id).
26
28
  saml_options
29
+ rescue
30
+ {}
27
31
  end
28
32
  end
29
33
 
30
- namespace '/auth' do # rubocop:disable Metrics/BlockLength
34
+ OmniAuth.config.on_failure = proc do |env|
35
+ OmniAuth::FailureEndpoint.new(env).redirect_to_failure
36
+ end
37
+
38
+ namespace '/auth' do
31
39
  get '/failure' do
32
- @error = params[:message]
40
+ # ??? invalid ticket, warden throws, ugh
41
+
42
+ # confirmed:
43
+ # - a valid but wrong cert will throw here
44
+ # (OneLogin::RubySaml::ValidationError, Fingerprint mismatch)
45
+ # but an _invalid_ cert is not caught. we do validate certs on
46
+ # configuration, so this may be ok
47
+ #
48
+ # - a valid but wrong ACS URL will throw here. the urls
49
+ # are pretty complex, but it has come up
50
+ #
51
+ # - specifying the wrong "recipient" in your IDP. Only OL so far
52
+ # (OneLogin::RubySaml::ValidationError, The response was received
53
+ # at vcardme.com instead of
54
+ # http://localhost:9292/auth/saml/e54a9a92-b4b5-4ea5-b0e3-b1423eb20b76/callback)
55
+
56
+
57
+ @error = Osso::Error::SamlConfigError.new
33
58
  erb :error
34
59
  end
35
60
  # Enterprise users are sent here after authenticating against
@@ -37,48 +62,18 @@ module Osso
37
62
  # and then create an authorization code for that user. The user
38
63
  # is redirected back to your application with this code
39
64
  # 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,
65
+ post '/saml/:provider_id/callback' do
66
+ redirect_uri = SamlHandler.perform(
67
+ auth_hash: env['omniauth.auth'],
68
+ provider_id: params[:provider_id],
69
+ session: session,
61
70
  )
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
-
73
- def provider_state
74
- return @provider_state = 'IDP_INITIATED' if valid_idp_initiated_flow
75
71
 
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]
81
- end
72
+ redirect(redirect_uri)
73
+ rescue Osso::Error::Base => e
74
+ @error = e
75
+ erb :error
76
+ end
82
77
  end
83
78
  end
84
79
  end
@@ -16,28 +16,19 @@ 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
- @providers = enterprise.identity_providers
32
- erb :multiple_providers
26
+ @providers = enterprise.identity_providers.not_pending
27
+ erb :multiple_providers if @providers.count > 1
33
28
 
34
- rescue Rack::OAuth2::Server::Authorize::BadRequest => e
35
- @error = e
36
- erb :error
37
- rescue ActiveRecord::RecordNotFound => e
29
+ raise Osso::Error::MissingConfiguredIdentityProvider.new(domain: params[:domain])
30
+ rescue Osso::Error::Base => e
38
31
  @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
32
  erb :error
42
33
  end
43
34
 
@@ -66,5 +57,31 @@ module Osso
66
57
  user
67
58
  end
68
59
  end
60
+
61
+ private
62
+
63
+ def find_account(domain:, client_id:)
64
+ Models::EnterpriseAccount.
65
+ includes(:identity_providers).
66
+ find_by!(domain: domain, oauth_client_id: client_id)
67
+ rescue ActiveRecord::RecordNotFound
68
+ raise Osso::Error::NoAccountForOAuthClientError.new(domain: params[:domain])
69
+ end
70
+
71
+ def find_client(identifier)
72
+ @client ||= Models::OauthClient.find_by!(identifier: identifier)
73
+ rescue ActiveRecord::RecordNotFound
74
+ raise Osso::Error::InvalidOAuthClientIdentifier
75
+ end
76
+
77
+ def validate_oauth_request(env)
78
+ Rack::OAuth2::Server::Authorize.new do |req, _res|
79
+ client = find_client(req[:client_id])
80
+ session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
81
+ session[:osso_oauth_state] = params[:state]
82
+ end.call(env)
83
+ rescue Rack::OAuth2::Server::Authorize::BadRequest
84
+ raise Osso::Error::InvalidRedirectUri.new(redirect_uri: params[:redirect_uri])
85
+ end
69
86
  end
70
87
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
- VERSION = '0.0.5-gamma'
4
+ VERSION = '0.0.5-theta'
5
5
  end
@@ -11,7 +11,8 @@ FactoryBot.define do
11
11
  factory :enterprise_with_okta, parent: :enterprise_account do
12
12
  after :create do |enterprise|
13
13
  create(
14
- :okta_identity_provider,
14
+ :configured_identity_provider,
15
+ service: 'OKTA',
15
16
  domain: enterprise.domain,
16
17
  enterprise_account_id: enterprise.id,
17
18
  )
@@ -21,7 +22,8 @@ FactoryBot.define do
21
22
  factory :enterprise_with_azure, parent: :enterprise_account do
22
23
  after :create do |enterprise|
23
24
  create(
24
- :azure_identity_provider,
25
+ :configured_identity_provider,
26
+ service: 'AZURE',
25
27
  domain: enterprise.domain,
26
28
  enterprise_account_id: enterprise.id,
27
29
  )
@@ -31,13 +33,15 @@ FactoryBot.define do
31
33
  factory :enterprise_with_multiple_providers, parent: :enterprise_account do
32
34
  after :create do |enterprise|
33
35
  create(
34
- :okta_identity_provider,
36
+ :configured_identity_provider,
37
+ service: 'OKTA',
35
38
  domain: enterprise.domain,
36
39
  enterprise_account_id: enterprise.id,
37
40
  )
38
41
 
39
42
  create(
40
- :azure_identity_provider,
43
+ :configured_identity_provider,
44
+ service: 'AZURE',
41
45
  domain: enterprise.domain,
42
46
  enterprise_account_id: enterprise.id,
43
47
  )
@@ -15,6 +15,7 @@ describe Osso::GraphQL::Schema do
15
15
  service
16
16
  domain
17
17
  acsUrl
18
+ acsUrlValidator
18
19
  ssoCert
19
20
  ssoUrl
20
21
  status
@@ -8,13 +8,21 @@ describe Osso::Helpers::Auth do
8
8
  end
9
9
 
10
10
  subject(:app) do
11
- Class.new { include Osso::Helpers::Auth }
11
+ Class.new {
12
+ include Osso::Helpers::Auth
13
+ }
12
14
  end
13
15
 
14
16
  describe 'with the token as a header' do
15
17
  before do
16
18
  allow_any_instance_of(subject).to receive(:request) do
17
- double('Request', env: { 'admin_token' => token }, post?: false)
19
+ double('Request', env: { 'HTTP_AUTHORIZATION' => token }, post?: false)
20
+ end
21
+
22
+ allow_any_instance_of(subject).to receive(:session) do
23
+ {
24
+ admin_token: nil
25
+ }
18
26
  end
19
27
 
20
28
  allow_any_instance_of(subject).to receive(:redirect) do
@@ -87,6 +95,170 @@ describe Osso::Helpers::Auth do
87
95
  end
88
96
  end
89
97
 
98
+ describe 'with the token as a parameter' do
99
+ before do
100
+ allow_any_instance_of(subject).to receive(:request) do
101
+ double('Request', env: {}, params: { 'admin_token' => token }, post?: false)
102
+ end
103
+
104
+ allow_any_instance_of(subject).to receive(:session) do
105
+ {
106
+ admin_token: nil
107
+ }
108
+ end
109
+
110
+ allow_any_instance_of(subject).to receive(:redirect) do
111
+ false
112
+ end
113
+ end
114
+
115
+ describe 'with an admin token' do
116
+ let(:token) { encode({ scope: 'admin' }) }
117
+
118
+ it 'allows #token_protected! methods' do
119
+ expect(subject.new.token_protected!).to_not be(false)
120
+ end
121
+
122
+ it 'allows #enterprise_protected! methods' do
123
+ expect(subject.new.enterprise_protected!).to_not be(false)
124
+ end
125
+
126
+ it 'allows #internal_protected! methods' do
127
+ expect(subject.new.internal_protected!).to_not be(false)
128
+ end
129
+
130
+ it 'allows #admin_protected! methods' do
131
+ expect(subject.new.admin_protected!).to_not be(false)
132
+ end
133
+ end
134
+
135
+ describe 'with an internal token' do
136
+ let(:token) { encode({ scope: 'internal' }) }
137
+
138
+ it 'allows #token_protected! methods' do
139
+ expect(subject.new.token_protected!).to_not be(false)
140
+ end
141
+
142
+ it 'allows #enterprise_protected! methods' do
143
+ expect(subject.new.enterprise_protected!).to_not be(false)
144
+ end
145
+
146
+ it 'allows #internal_protected! methods' do
147
+ expect(subject.new.internal_protected!).to_not be(false)
148
+ end
149
+
150
+ it 'allows #admin_protected! methods' do
151
+ expect(subject.new.admin_protected!).to be(false)
152
+ end
153
+ end
154
+
155
+ describe 'with an end-user token' do
156
+ let(:token) { encode({ scope: 'end-user', email: 'user@example.com' }) }
157
+
158
+ it 'allows #token_protected! methods' do
159
+ expect(subject.new.token_protected!).to_not be(false)
160
+ end
161
+
162
+ it 'allows #enterprise_protected! methods for the scoped domain' do
163
+ expect(subject.new.enterprise_protected!('example.com')).to_not be(false)
164
+ end
165
+
166
+ it 'halts #enterprise_protected! methods for the wrong scoped domain' do
167
+ expect(subject.new.enterprise_protected!('foo.com')).to be(false)
168
+ end
169
+
170
+ it 'halts #internal_protected! methods' do
171
+ expect(subject.new.internal_protected!).to be(false)
172
+ end
173
+
174
+ it 'halts #admin_protected! methods' do
175
+ expect(subject.new.admin_protected!).to be(false)
176
+ end
177
+ end
178
+ end
179
+
180
+ describe 'with the token in session' do
181
+ before do
182
+ allow_any_instance_of(subject).to receive(:request) do
183
+ double('Request', env: {}, params: {}, post?: false)
184
+ end
185
+
186
+ allow_any_instance_of(subject).to receive(:redirect) do
187
+ false
188
+ end
189
+
190
+ allow_any_instance_of(subject).to receive(:session).and_return(
191
+ {admin_token: token}.with_indifferent_access
192
+ )
193
+
194
+ end
195
+
196
+ describe 'with an admin token' do
197
+ let(:token) { encode({ scope: 'admin' }) }
198
+
199
+
200
+ it 'allows #token_protected! methods' do
201
+ expect(subject.new.token_protected!).to_not be(false)
202
+ end
203
+
204
+ it 'allows #enterprise_protected! methods' do
205
+ expect(subject.new.enterprise_protected!).to_not be(false)
206
+ end
207
+
208
+ it 'allows #internal_protected! methods' do
209
+ expect(subject.new.internal_protected!).to_not be(false)
210
+ end
211
+
212
+ it 'allows #admin_protected! methods' do
213
+ expect(subject.new.admin_protected!).to_not be(false)
214
+ end
215
+ end
216
+
217
+ describe 'with an internal token' do
218
+ let(:token) { encode({ scope: 'internal' }) }
219
+
220
+ it 'allows #token_protected! methods' do
221
+ expect(subject.new.token_protected!).to_not be(false)
222
+ end
223
+
224
+ it 'allows #enterprise_protected! methods' do
225
+ expect(subject.new.enterprise_protected!).to_not be(false)
226
+ end
227
+
228
+ it 'allows #internal_protected! methods' do
229
+ expect(subject.new.internal_protected!).to_not be(false)
230
+ end
231
+
232
+ it 'allows #admin_protected! methods' do
233
+ expect(subject.new.admin_protected!).to be(false)
234
+ end
235
+ end
236
+
237
+ describe 'with an end-user token' do
238
+ let(:token) { encode({ scope: 'end-user', email: 'user@example.com' }) }
239
+
240
+ it 'allows #token_protected! methods' do
241
+ expect(subject.new.token_protected!).to_not be(false)
242
+ end
243
+
244
+ it 'allows #enterprise_protected! methods for the scoped domain' do
245
+ expect(subject.new.enterprise_protected!('example.com')).to_not be(false)
246
+ end
247
+
248
+ it 'halts #enterprise_protected! methods for the wrong scoped domain' do
249
+ expect(subject.new.enterprise_protected!('foo.com')).to be(false)
250
+ end
251
+
252
+ it 'halts #internal_protected! methods' do
253
+ expect(subject.new.internal_protected!).to be(false)
254
+ end
255
+
256
+ it 'halts #admin_protected! methods' do
257
+ expect(subject.new.admin_protected!).to be(false)
258
+ end
259
+ end
260
+ end
261
+
90
262
  def encode(payload)
91
263
  JWT.encode(
92
264
  payload,
@@ -24,6 +24,19 @@ describe Osso::Models::IdentityProvider do
24
24
  end
25
25
  end
26
26
 
27
+ describe '#acs_url_validator' do
28
+ it 'returns a regex escaped string' do
29
+ allow(subject).to receive(:acs_url).and_return(
30
+ 'https://foo.com/auth/saml/callback'
31
+ )
32
+
33
+ expect(subject.acs_url_validator).to eq(
34
+ 'https://foo\\.com/auth/saml/callback'
35
+ )
36
+ end
37
+ end
38
+
39
+
27
40
  describe '#saml_options' do
28
41
  it 'returns the required args' do
29
42
  expect(subject.saml_options).
@@ -3,6 +3,9 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Osso::Auth do
6
+ before do
7
+ described_class.set(:views, spec_views)
8
+ end
6
9
  describe 'get /auth/saml/:uuid' do
7
10
  describe 'for an Okta SAML provider' do
8
11
  let(:enterprise) { create(:enterprise_with_okta) }
@@ -43,7 +46,6 @@ describe Osso::Auth do
43
46
  nil,
44
47
  {
45
48
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
46
- 'identity_provider' => okta_provider,
47
49
  },
48
50
  )
49
51
  end.to change { Osso::Models::User.count }.by(1)
@@ -58,7 +60,6 @@ describe Osso::Auth do
58
60
  nil,
59
61
  {
60
62
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
61
- 'identity_provider' => okta_provider,
62
63
  },
63
64
  )
64
65
  end.to change { Osso::Models::AuthorizationCode.count }.by(1)
@@ -73,7 +74,6 @@ describe Osso::Auth do
73
74
  nil,
74
75
  {
75
76
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
76
- 'identity_provider' => okta_provider,
77
77
  },
78
78
  )
79
79
  expect(last_response).to be_redirect
@@ -99,7 +99,6 @@ describe Osso::Auth do
99
99
  nil,
100
100
  {
101
101
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
102
- 'identity_provider' => okta_provider,
103
102
  },
104
103
  )
105
104
  end.to_not(change { Osso::Models::User.count })
@@ -110,10 +109,9 @@ describe Osso::Auth do
110
109
  nil,
111
110
  {
112
111
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
113
- 'identity_provider' => okta_provider,
114
112
  },
115
113
  )
116
- expect(okta_provider.reload.status).to eq('ACTIVE')
114
+ expect(okta_provider.reload.status).to eq('active')
117
115
  end
118
116
  end
119
117
  end
@@ -132,7 +130,6 @@ describe Osso::Auth do
132
130
  nil,
133
131
  {
134
132
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
135
- 'identity_provider' => azure_provider,
136
133
  },
137
134
  )
138
135
  end.to change { Osso::Models::User.count }.by(1)
@@ -146,11 +143,10 @@ describe Osso::Auth do
146
143
  nil,
147
144
  {
148
145
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
149
- 'identity_provider' => azure_provider,
150
146
  },
151
147
  )
152
148
 
153
- expect(azure_provider.reload.status).to eq('ACTIVE')
149
+ expect(azure_provider.reload.status).to eq('active')
154
150
  end
155
151
  end
156
152
 
@@ -170,7 +166,6 @@ describe Osso::Auth do
170
166
  nil,
171
167
  {
172
168
  'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
173
- 'identity_provider' => azure_provider,
174
169
  },
175
170
  )
176
171
  end.to_not(change { Osso::Models::User.count })
@@ -178,4 +173,40 @@ describe Osso::Auth do
178
173
  end
179
174
  end
180
175
  end
176
+
177
+ context 'with an invalid SAML response' do
178
+ describe 'post /auth/saml/:uuid/callback' do
179
+ let!(:enterprise) { create(:enterprise_with_azure) }
180
+ let!(:azure_provider) { enterprise.provider }
181
+
182
+ it 'raises an error when email is missing' do
183
+ mock_saml_omniauth(email: nil, id: SecureRandom.uuid)
184
+
185
+
186
+ response = post(
187
+ "/auth/saml/#{azure_provider.id}/callback",
188
+ nil,
189
+ {
190
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
191
+ },
192
+ )
193
+
194
+ expect(response.body).to eq('Osso::Error::MissingSamlEmailAttributeError')
195
+ end
196
+
197
+ it 'raises an error when id is missing' do
198
+ mock_saml_omniauth(email: Faker::Internet.email, id: nil)
199
+
200
+ response = post(
201
+ "/auth/saml/#{azure_provider.id}/callback",
202
+ nil,
203
+ {
204
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
205
+ },
206
+ )
207
+
208
+ expect(response.body).to eq('Osso::Error::MissingSamlIdAttributeError')
209
+ end
210
+ end
211
+ end
181
212
  end
@@ -0,0 +1 @@
1
+ <%= @error %>
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.gamma
4
+ version: 0.0.5.pre.theta
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-16 00:00:00.000000000 Z
11
+ date: 2020-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -273,6 +273,11 @@ 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/account_configuration_error.rb
277
+ - lib/osso/error/error.rb
278
+ - lib/osso/error/missing_saml_attribute_error.rb
279
+ - lib/osso/error/oauth_error.rb
280
+ - lib/osso/error/saml_config_error.rb
276
281
  - lib/osso/graphql/.DS_Store
277
282
  - lib/osso/graphql/mutation.rb
278
283
  - lib/osso/graphql/mutations.rb
@@ -313,6 +318,7 @@ files:
313
318
  - lib/osso/lib/app_config.rb
314
319
  - lib/osso/lib/oauth2_token.rb
315
320
  - lib/osso/lib/route_map.rb
321
+ - lib/osso/lib/saml_handler.rb
316
322
  - lib/osso/models/access_token.rb
317
323
  - lib/osso/models/app_config.rb
318
324
  - lib/osso/models/authorization_code.rb
@@ -347,6 +353,7 @@ files:
347
353
  - spec/graphql/query/identity_provider_spec.rb
348
354
  - spec/graphql/query/oauth_clients_spec.rb
349
355
  - spec/helpers/auth_spec.rb
356
+ - spec/lib/saml_handler_spec.rb
350
357
  - spec/models/identity_provider_spec.rb
351
358
  - spec/routes/admin_spec.rb
352
359
  - spec/routes/app_spec.rb