osso 0.0.5.pre.zeta → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +6 -4
  3. data/.github/dependabot.yml +8 -0
  4. data/.github/workflows/automerge.yml +19 -0
  5. data/.rubocop.yml +4 -1
  6. data/Gemfile +2 -2
  7. data/Gemfile.lock +69 -51
  8. data/LICENSE +21 -23
  9. data/Rakefile +2 -0
  10. data/bin/annotate +3 -1
  11. data/db/schema.rb +41 -3
  12. data/lib/osso.rb +0 -1
  13. data/lib/osso/db/migrate/20200929154117_add_users_count_to_identity_providers_and_enterprise_accounts.rb +6 -0
  14. data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
  15. data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
  16. data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
  17. data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
  18. data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
  19. data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
  20. data/lib/osso/db/migrate/20201125143501_add_salesforce_to_provider_service_enum.rb +28 -0
  21. data/lib/osso/error/account_configuration_error.rb +1 -0
  22. data/lib/osso/error/oauth_error.rb +6 -3
  23. data/lib/osso/graphql/mutation.rb +2 -0
  24. data/lib/osso/graphql/mutations.rb +2 -0
  25. data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
  26. data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
  27. data/lib/osso/graphql/mutations/delete_identity_provider.rb +24 -0
  28. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  29. data/lib/osso/graphql/query.rb +8 -0
  30. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +3 -3
  31. data/lib/osso/graphql/types.rb +2 -2
  32. data/lib/osso/graphql/types/admin_user.rb +9 -0
  33. data/lib/osso/graphql/types/base_object.rb +1 -1
  34. data/lib/osso/graphql/types/enterprise_account.rb +1 -0
  35. data/lib/osso/graphql/types/identity_provider.rb +3 -0
  36. data/lib/osso/graphql/types/identity_provider_service.rb +3 -1
  37. data/lib/osso/lib/app_config.rb +1 -1
  38. data/lib/osso/lib/route_map.rb +0 -15
  39. data/lib/osso/lib/saml_handler.rb +5 -0
  40. data/lib/osso/models/access_token.rb +4 -2
  41. data/lib/osso/models/account.rb +34 -0
  42. data/lib/osso/models/authorization_code.rb +2 -1
  43. data/lib/osso/models/enterprise_account.rb +3 -1
  44. data/lib/osso/models/identity_provider.rb +24 -5
  45. data/lib/osso/models/models.rb +1 -0
  46. data/lib/osso/models/oauth_client.rb +0 -1
  47. data/lib/osso/models/user.rb +2 -2
  48. data/lib/osso/routes/admin.rb +39 -33
  49. data/lib/osso/routes/auth.rb +9 -9
  50. data/lib/osso/routes/oauth.rb +42 -18
  51. data/lib/osso/version.rb +1 -1
  52. data/lib/osso/views/admin.erb +5 -0
  53. data/lib/osso/views/error.erb +1 -0
  54. data/lib/osso/views/layout.erb +0 -0
  55. data/lib/osso/views/multiple_providers.erb +1 -0
  56. data/lib/osso/views/welcome.erb +0 -0
  57. data/lib/tasks/bootstrap.rake +25 -4
  58. data/osso-rb.gemspec +5 -0
  59. data/spec/factories/account.rb +24 -0
  60. data/spec/factories/enterprise_account.rb +11 -3
  61. data/spec/factories/identity_providers.rb +10 -2
  62. data/spec/factories/user.rb +4 -0
  63. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  64. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  65. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  66. data/spec/graphql/query/identity_provider_spec.rb +3 -2
  67. data/spec/models/enterprise_account_spec.rb +18 -0
  68. data/spec/models/identity_provider_spec.rb +36 -1
  69. data/spec/routes/admin_spec.rb +7 -41
  70. data/spec/routes/auth_spec.rb +17 -18
  71. data/spec/routes/oauth_spec.rb +102 -5
  72. data/spec/spec_helper.rb +3 -3
  73. data/spec/support/views/hosted_login.erb +1 -0
  74. data/spec/support/views/layout.erb +1 -0
  75. data/spec/support/views/multiple_providers.erb +1 -0
  76. metadata +108 -7
  77. data/lib/osso/helpers/auth.rb +0 -94
  78. data/lib/osso/helpers/helpers.rb +0 -8
  79. data/spec/helpers/auth_spec.rb +0 -97
@@ -5,9 +5,11 @@ module Osso
5
5
  module Types
6
6
  class IdentityProviderService < BaseEnum
7
7
  value('AZURE', 'Microsoft Azure Identity Provider', value: 'AZURE')
8
+ value('GOOGLE', 'Google SAML Identity Provider', value: 'GOOGLE')
8
9
  value('OKTA', 'Okta Identity Provider', value: 'OKTA')
9
10
  value('ONELOGIN', 'OneLogin Identity Provider', value: 'ONELOGIN')
10
- value('GOOGLE', 'Google SAML Identity Provider', value: 'GOOGLE')
11
+ value('PING', 'PingID Identity Provider', value: 'PING')
12
+ value('SALESFORCE', 'Salesforce Identity Provider', value: 'SALESFORCE')
11
13
  end
12
14
  end
13
15
  end
@@ -7,7 +7,7 @@ module Osso
7
7
  def self.included(klass)
8
8
  klass.class_eval do
9
9
  use Rack::JSONBodyParser
10
- use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
10
+ use Rack::Session::Cookie, secret: ENV.fetch('SESSION_SECRET')
11
11
 
12
12
  error ActiveRecord::RecordNotFound do
13
13
  status 404
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/MethodLength
4
-
5
3
  module Osso
6
4
  module RouteMap
7
5
  def self.included(klass)
@@ -9,20 +7,7 @@ module Osso
9
7
  use Osso::Admin
10
8
  use Osso::Auth
11
9
  use Osso::Oauth
12
-
13
- post '/graphql' do
14
- token_protected!
15
-
16
- result = Osso::GraphQL::Schema.execute(
17
- params[:query],
18
- variables: params[:variables],
19
- context: current_user.symbolize_keys,
20
- )
21
-
22
- json result
23
- end
24
10
  end
25
11
  end
26
12
  end
27
13
  end
28
- # rubocop:enable Metrics/MethodLength
@@ -55,6 +55,7 @@ module Osso
55
55
  @authorization_code ||= user.authorization_codes.create!(
56
56
  oauth_client: provider.oauth_client,
57
57
  redirect_uri: redirect_uri_base,
58
+ requested: requested_param,
58
59
  )
59
60
  end
60
61
 
@@ -81,5 +82,9 @@ module Osso
81
82
  def valid_idp_initiated_flow
82
83
  !session[:osso_oauth_redirect_uri] && !session[:osso_oauth_state]
83
84
  end
85
+
86
+ def requested_param
87
+ @session.delete(:osso_oauth_requested)
88
+ end
84
89
  end
85
90
  end
@@ -39,9 +39,11 @@ end
39
39
  # updated_at :datetime not null
40
40
  # user_id :uuid
41
41
  # oauth_client_id :uuid
42
+ # requested :jsonb
42
43
  #
43
44
  # Indexes
44
45
  #
45
- # index_access_tokens_on_oauth_client_id (oauth_client_id)
46
- # index_access_tokens_on_user_id (user_id)
46
+ # index_access_tokens_on_oauth_client_id (oauth_client_id)
47
+ # index_access_tokens_on_token_and_expires_at (token,expires_at) UNIQUE
48
+ # index_access_tokens_on_user_id (user_id)
47
49
  #
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Models
5
+ class Account < ::ActiveRecord::Base
6
+ enum status_id: { 1 => :Unverified, 2 => :Verified, 3 => :Closed }
7
+
8
+ def context
9
+ {
10
+ email: email,
11
+ id: id,
12
+ scope: role,
13
+ oauth_client_id: oauth_client_id,
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # == Schema Information
21
+ #
22
+ # Table name: accounts
23
+ #
24
+ # id :uuid not null, primary key
25
+ # email :citext not null
26
+ # status_id :integer default(NULL), not null
27
+ # role :string default("admin"), not null
28
+ # oauth_client_id :string
29
+ #
30
+ # Indexes
31
+ #
32
+ # index_accounts_on_email (email) UNIQUE WHERE (status_id = ANY (ARRAY[1, 2]))
33
+ # index_accounts_on_oauth_client_id (oauth_client_id)
34
+ #
@@ -7,7 +7,7 @@ module Osso
7
7
 
8
8
  def access_token
9
9
  @access_token ||= expired! &&
10
- user.access_tokens.create(oauth_client: oauth_client)
10
+ user.access_tokens.create(oauth_client: oauth_client, requested: requested)
11
11
  end
12
12
  end
13
13
  end
@@ -25,6 +25,7 @@ end
25
25
  # updated_at :datetime not null
26
26
  # user_id :uuid
27
27
  # oauth_client_id :uuid
28
+ # requested :jsonb
28
29
  #
29
30
  # Indexes
30
31
  #
@@ -8,10 +8,11 @@ module Osso
8
8
  # includes fields for external IDs such that you can persist
9
9
  # your ID for an account in your Osso instance.
10
10
  class EnterpriseAccount < ActiveRecord::Base
11
- belongs_to :oauth_client
12
11
  has_many :users
13
12
  has_many :identity_providers
14
13
 
14
+ validates_format_of :domain, with: /\A[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/
15
+
15
16
  def single_provider?
16
17
  identity_providers.not_pending.one?
17
18
  end
@@ -40,6 +41,7 @@ end
40
41
  # name :string not null
41
42
  # created_at :datetime not null
42
43
  # updated_at :datetime not null
44
+ # users_count :integer default(0)
43
45
  #
44
46
  # Indexes
45
47
  #
@@ -6,15 +6,20 @@ 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
+ before_create :set_sso_issuer
10
11
  before_save :set_status
11
12
  validate :sso_cert_valid
12
13
 
13
- enum status: { pending: "PENDING", configured: 'CONFIGURED', active: "ACTIVE", error: "ERROR"}
14
+ enum status: { pending: 'PENDING', configured: 'CONFIGURED', active: 'ACTIVE', error: 'ERROR' }
14
15
 
15
16
  PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"
16
17
  PEM_FOOTER = "\n-----END CERTIFICATE-----"
17
18
 
19
+ ENTITY_ID_URI_REQUIRED = [
20
+ 'PING',
21
+ ]
22
+
18
23
  def name
19
24
  service.titlecase
20
25
  end
@@ -24,7 +29,8 @@ module Osso
24
29
  domain: domain,
25
30
  idp_sso_target_url: sso_url,
26
31
  idp_cert: sso_cert,
27
- issuer: domain,
32
+ issuer: sso_issuer,
33
+ name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
28
34
  }
29
35
  end
30
36
 
@@ -40,10 +46,22 @@ module Osso
40
46
 
41
47
  alias acs_url assertion_consumer_service_url
42
48
 
49
+ def acs_url_validator
50
+ Regexp.escape(acs_url)
51
+ end
52
+
43
53
  def set_status
44
54
  self.status = 'configured' if sso_url && sso_cert && pending?
45
55
  end
46
56
 
57
+ def set_sso_issuer
58
+ parts = [domain, oauth_client_id]
59
+
60
+ parts.unshift('https:/') if ENTITY_ID_URI_REQUIRED.any?(service)
61
+
62
+ self.sso_issuer = parts.join('/')
63
+ end
64
+
47
65
  def active!
48
66
  update(status: 'active')
49
67
  end
@@ -81,15 +99,16 @@ end
81
99
  # Table name: identity_providers
82
100
  #
83
101
  # id :uuid not null, primary key
84
- # service :string
102
+ # service :enum
85
103
  # domain :string not null
86
104
  # sso_url :string
87
105
  # sso_cert :text
88
106
  # enterprise_account_id :uuid
89
107
  # oauth_client_id :uuid
90
- # status :enum default("PENDING")
108
+ # status :enum default("pending")
91
109
  # created_at :datetime
92
110
  # updated_at :datetime
111
+ # users_count :integer default(0)
93
112
  #
94
113
  # Indexes
95
114
  #
@@ -10,6 +10,7 @@ module Osso
10
10
  end
11
11
 
12
12
  require_relative 'access_token'
13
+ require_relative 'account'
13
14
  require_relative 'app_config'
14
15
  require_relative 'authorization_code'
15
16
  require_relative 'enterprise_account'
@@ -5,7 +5,6 @@ module Osso
5
5
  module Models
6
6
  class OauthClient < ActiveRecord::Base
7
7
  has_many :access_tokens
8
- has_many :enterprise_accounts
9
8
  has_many :refresh_tokens
10
9
  has_many :identity_providers
11
10
  has_many :redirect_uris
@@ -3,8 +3,8 @@
3
3
  module Osso
4
4
  module Models
5
5
  class User < ActiveRecord::Base
6
- belongs_to :enterprise_account
7
- belongs_to :identity_provider
6
+ belongs_to :enterprise_account, counter_cache: true
7
+ belongs_to :identity_provider, counter_cache: true
8
8
  has_many :authorization_codes, dependent: :delete_all
9
9
  has_many :access_tokens, dependent: :delete_all
10
10
 
@@ -1,53 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jwt'
3
+ require 'roda'
4
+ require 'sequel/core'
4
5
 
5
- module Osso
6
- class Admin < Sinatra::Base
7
- include AppConfig
8
- helpers Helpers::Auth
9
- register Sinatra::Namespace
6
+ DEFAULT_VIEWS_DIR = File.join(File.expand_path(Bundler.root), 'views/rodauth')
10
7
 
11
- before do
12
- chomp_token
8
+ module Osso
9
+ class Admin < Roda
10
+ DB = Sequel.postgres(extensions: :activerecord_connection)
11
+ use Rack::Session::Cookie, secret: ENV.fetch('SESSION_SECRET')
12
+
13
+ plugin :middleware
14
+ plugin :render, engine: 'erb', views: ENV['RODAUTH_VIEWS'] || DEFAULT_VIEWS_DIR
15
+ plugin :route_csrf
16
+
17
+ plugin :rodauth do
18
+ enable :login, :verify_account
19
+ verify_account_set_password? true
20
+ already_logged_in { redirect login_redirect }
21
+ use_database_authentication_functions? false
22
+
23
+ before_create_account_route do
24
+ request.halt unless DB[:accounts].empty?
25
+ end
13
26
  end
14
27
 
15
- namespace '/admin' do
16
- get '/login' do
17
- token_protected!
18
-
19
- erb :admin, layout: false
20
- end
28
+ alias erb render
21
29
 
22
- get '' do
23
- internal_protected!
30
+ route do |r|
31
+ r.rodauth
24
32
 
25
- erb :admin, layout: false
33
+ def current_account
34
+ Osso::Models::Account.find(rodauth.session['account_id']).
35
+ context.
36
+ merge({ rodauth: rodauth })
26
37
  end
27
38
 
28
- get '/enterprise' do
29
- token_protected!
30
-
39
+ r.on 'admin' do
40
+ rodauth.require_authentication
31
41
  erb :admin, layout: false
32
42
  end
33
43
 
34
- get '/enterprise/:domain' do
35
- enterprise_protected!(params[:domain])
44
+ r.post 'graphql' do
45
+ rodauth.require_authentication
36
46
 
37
- erb :admin, layout: false
38
- end
47
+ result = Osso::GraphQL::Schema.execute(
48
+ r.params['query'],
49
+ variables: r.params['variables'],
50
+ context: current_account,
51
+ )
39
52
 
40
- get '/config' do
41
- admin_protected!
42
-
43
- erb :admin, layout: false
53
+ result.to_json
44
54
  end
45
55
 
46
- get '/config/:id' do
47
- admin_protected!
48
-
49
- erb :admin, layout: false
50
- end
56
+ env['rodauth'] = rodauth
51
57
  end
52
58
  end
53
59
  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,
@@ -26,7 +26,8 @@ module Osso
26
26
  not_pending.
27
27
  find(identity_provider_id).
28
28
  saml_options
29
- rescue
29
+ rescue StandardError => e
30
+ Raven.capture_exception(e) if defined?(Raven)
30
31
  {}
31
32
  end
32
33
  end
@@ -38,21 +39,20 @@ module Osso
38
39
  namespace '/auth' do
39
40
  get '/failure' do
40
41
  # ??? invalid ticket, warden throws, ugh
41
-
42
+
42
43
  # confirmed:
43
- # - a valid but wrong cert will throw here
44
+ # - a valid but wrong cert will throw here
44
45
  # (OneLogin::RubySaml::ValidationError, Fingerprint mismatch)
45
46
  # but an _invalid_ cert is not caught. we do validate certs on
46
47
  # configuration, so this may be ok
47
48
  #
48
49
  # - a valid but wrong ACS URL will throw here. the urls
49
50
  # are pretty complex, but it has come up
50
- #
51
+ #
51
52
  # - specifying the wrong "recipient" in your IDP. Only OL so far
52
53
  # (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
-
54
+ # at vcardme.com instead of
55
+ # http://localhost:9292/auth/saml/e54a9a92-b4b5-4ea5-b0e3-b1423eb20b76/callback)
56
56
 
57
57
  @error = Osso::Error::SamlConfigError.new
58
58
  erb :error
@@ -73,7 +73,7 @@ module Osso
73
73
  rescue Osso::Error::Base => e
74
74
  @error = e
75
75
  erb :error
76
- end
76
+ end
77
77
  end
78
78
  end
79
79
  end
@@ -16,15 +16,15 @@ 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
- client = find_client(params[:client_id])
20
- enterprise = find_account(domain: params[:domain], client_id: client.id)
21
-
22
19
  validate_oauth_request(env)
23
20
 
24
- redirect "/auth/saml/#{enterprise.provider.id}" if enterprise.single_provider?
21
+ return erb :hosted_login if render_hosted_login?
22
+
23
+ @providers = find_providers
25
24
 
26
- @providers = enterprise.identity_providers.not_pending
27
- erb :multiple_providers if @providers.count > 1
25
+ redirect "/auth/saml/#{@providers.first.id}" if @providers.one?
26
+
27
+ return erb :multiple_providers if @providers.count > 1
28
28
 
29
29
  raise Osso::Error::MissingConfiguredIdentityProvider.new(domain: params[:domain])
30
30
  rescue Osso::Error::Base => e
@@ -38,11 +38,12 @@ module Osso
38
38
  # and client secret
39
39
  post '/token' do
40
40
  Rack::OAuth2::Server::Token.new do |req, res|
41
- code = Models::AuthorizationCode.
42
- find_by_token!(params[:code])
43
41
  client = Models::OauthClient.find_by!(identifier: req.client_id)
44
42
  req.invalid_client! if client.secret != req.client_secret
43
+
44
+ code = Models::AuthorizationCode.find_by_token!(params[:code])
45
45
  req.invalid_grant! if code.redirect_uri != req.redirect_uri
46
+
46
47
  res.access_token = code.access_token.to_bearer_token
47
48
  end.call(env)
48
49
  end
@@ -50,22 +51,40 @@ module Osso
50
51
  # Use the access token to request a profile for the user who
51
52
  # just logged in. Access tokens are short-lived.
52
53
  get '/me' do
53
- json Models::AccessToken.
54
+ token = Models::AccessToken.
54
55
  includes(:user).
55
56
  valid.
56
- find_by_token!(params[:access_token]).
57
- user
57
+ find_by_token!(access_token)
58
+
59
+ json token.user.as_json.merge(requested: token.requested)
58
60
  end
59
61
  end
60
62
 
61
63
  private
62
64
 
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])
65
+ def render_hosted_login?
66
+ [params[:email], params[:domain]].all?(&:nil?)
67
+ end
68
+
69
+ def find_providers
70
+ if params[:email]
71
+ user = Osso::Models::User.
72
+ includes(:identity_provider).
73
+ find_by(email: params[:email])
74
+ return [user.identity_provider] if user
75
+ end
76
+
77
+ Osso::Models::IdentityProvider.
78
+ joins(:oauth_client).
79
+ not_pending.
80
+ where(
81
+ domain: domain_from_params,
82
+ oauth_clients: { identifier: params[:client_id] },
83
+ )
84
+ end
85
+
86
+ def domain_from_params
87
+ params[:domain] || params[:email].split('@')[1]
69
88
  end
70
89
 
71
90
  def find_client(identifier)
@@ -74,14 +93,19 @@ module Osso
74
93
  raise Osso::Error::InvalidOAuthClientIdentifier
75
94
  end
76
95
 
77
- def validate_oauth_request(env)
96
+ def validate_oauth_request(env) # rubocop:disable Metrics/AbcSize
78
97
  Rack::OAuth2::Server::Authorize.new do |req, _res|
79
98
  client = find_client(req[:client_id])
80
99
  session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
81
100
  session[:osso_oauth_state] = params[:state]
101
+ session[:osso_oauth_requested] = { domain: req[:domain], email: req[:email] }
82
102
  end.call(env)
83
103
  rescue Rack::OAuth2::Server::Authorize::BadRequest
84
104
  raise Osso::Error::InvalidRedirectUri.new(redirect_uri: params[:redirect_uri])
85
105
  end
106
+
107
+ def access_token
108
+ params[:access_token] || env.fetch('HTTP_AUTHORIZATION', '').slice(-64..-1)
109
+ end
86
110
  end
87
111
  end