osso 0.0.5.pre.beta → 0.0.5.pre.iota

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: e2f9a55978dad2e0b1829183ff8c023a4a9981c9403e4fb7b7fce0448bd553d9
4
- data.tar.gz: 3355b2e9c7ebe56afbe62ad678c2bda13ab7d924ae8e341511763df8683ece81
3
+ metadata.gz: 235fe13e7b403d95ad9874e9b06d9fa98f57172a079729ee911a00405973613a
4
+ data.tar.gz: 1d54deb9b34c9db6a6fe14f04945ed31249dfa48b39c7a8be8b68883db5e0761
5
5
  SHA512:
6
- metadata.gz: f8128ec3cd5ae5111ebe6fab2af92fd8fd5b7e1c5b4f10b2c72867b1bc6dd8465207f4376f264c70a2d75aab7cef2385f905d8597f25fed6557a70bab0b807ef
7
- data.tar.gz: b0f6199724d71b0cd3b6c21d1b2194cf5ce731c896b84d1be0f1558d5bffaa69b58196487f60a68559404488a1da8cac55b09262bf6c822f4ce5492c7f271a60
6
+ metadata.gz: 32e88b6b88fda909b392bfe36a9881745b58c894216f72dd0a876068130fb800b764aee9f21e95ca5aab8b683f864f3b3d4087beedbd340a538ea77e25473201
7
+ data.tar.gz: b81e88465da27da4b3b21de5ff159719ac9f6ce7a61495be6a4d2d7ab38a3d41b4ab4453ac29b123181af61445ef78bf558e9a556a1ef7296170155989a29bf1
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- osso (0.0.5.pre.alpha)
4
+ osso (0.0.5.pre.theta)
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
@@ -11,6 +11,7 @@ module Osso
11
11
  field :create_enterprise_account, mutation: Mutations::CreateEnterpriseAccount
12
12
  field :create_oauth_client, mutation: Mutations::CreateOauthClient
13
13
  field :delete_enterprise_account, mutation: Mutations::DeleteEnterpriseAccount
14
+ field :delete_identity_provider, mutation: Mutations::DeleteIdentityProvider
14
15
  field :delete_oauth_client, mutation: Mutations::DeleteOauthClient
15
16
  field :set_redirect_uris, mutation: Mutations::SetRedirectUris
16
17
  field :regenerate_oauth_credentials, mutation: Mutations::RegenerateOauthCredentials
@@ -11,6 +11,7 @@ require_relative 'mutations/create_identity_provider'
11
11
  require_relative 'mutations/create_enterprise_account'
12
12
  require_relative 'mutations/create_oauth_client'
13
13
  require_relative 'mutations/delete_enterprise_account'
14
+ require_relative 'mutations/delete_identity_provider'
14
15
  require_relative 'mutations/delete_oauth_client'
15
16
  require_relative 'mutations/regenerate_oauth_credentials'
16
17
  require_relative 'mutations/set_redirect_uris'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module GraphQL
5
+ module Mutations
6
+ class DeleteIdentityProvider < BaseMutation
7
+ null false
8
+
9
+ argument :id, ID, required: true
10
+
11
+ field :identity_provider, Types::IdentityProvider, null: true
12
+ field :errors, [String], null: false
13
+
14
+ def resolve(id:)
15
+ identity_provider = Osso::Models::IdentityProvider.find(id)
16
+
17
+ return response_data(identity_provider: nil) if identity_provider.destroy
18
+
19
+ response_error(identity_provider.errors)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ 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
@@ -6,10 +6,12 @@ module Osso
6
6
  class IdentityProvider < ActiveRecord::Base
7
7
  belongs_to :enterprise_account
8
8
  belongs_to :oauth_client
9
- has_many :users
9
+ has_many :users, dependent: :delete_all
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
- # TODO: multiple provider support
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-beta'
4
+ VERSION = '0.0.5-iota'
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.beta
4
+ version: 0.0.5.pre.iota
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-28 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
@@ -282,6 +287,7 @@ files:
282
287
  - lib/osso/graphql/mutations/create_identity_provider.rb
283
288
  - lib/osso/graphql/mutations/create_oauth_client.rb
284
289
  - lib/osso/graphql/mutations/delete_enterprise_account.rb
290
+ - lib/osso/graphql/mutations/delete_identity_provider.rb
285
291
  - lib/osso/graphql/mutations/delete_oauth_client.rb
286
292
  - lib/osso/graphql/mutations/regenerate_oauth_credentials.rb
287
293
  - lib/osso/graphql/mutations/set_redirect_uris.rb
@@ -313,6 +319,7 @@ files:
313
319
  - lib/osso/lib/app_config.rb
314
320
  - lib/osso/lib/oauth2_token.rb
315
321
  - lib/osso/lib/route_map.rb
322
+ - lib/osso/lib/saml_handler.rb
316
323
  - lib/osso/models/access_token.rb
317
324
  - lib/osso/models/app_config.rb
318
325
  - lib/osso/models/authorization_code.rb
@@ -327,8 +334,6 @@ files:
327
334
  - lib/osso/routes/auth.rb
328
335
  - lib/osso/routes/oauth.rb
329
336
  - lib/osso/routes/routes.rb
330
- - lib/osso/routes/views/error.erb
331
- - lib/osso/routes/views/multiple_providers.erb
332
337
  - lib/osso/version.rb
333
338
  - lib/tasks/bootstrap.rake
334
339
  - osso-rb.gemspec
@@ -349,6 +354,7 @@ files:
349
354
  - spec/graphql/query/identity_provider_spec.rb
350
355
  - spec/graphql/query/oauth_clients_spec.rb
351
356
  - spec/helpers/auth_spec.rb
357
+ - spec/lib/saml_handler_spec.rb
352
358
  - spec/models/identity_provider_spec.rb
353
359
  - spec/routes/admin_spec.rb
354
360
  - spec/routes/app_spec.rb
@@ -359,6 +365,7 @@ files:
359
365
  - spec/support/spec_app.rb
360
366
  - spec/support/views/admin.erb
361
367
  - spec/support/views/error.erb
368
+ - spec/support/views/multiple_providers.erb
362
369
  homepage: https://github.com/enterprise-oss/osso-rb
363
370
  licenses:
364
371
  - MIT
@@ -1 +0,0 @@
1
- <%= @error %>
@@ -1 +0,0 @@
1
- multiple providers