osso 0.0.3.12 → 0.0.3.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +8 -1
  3. data/.rubocop.yml +1 -0
  4. data/Gemfile.lock +2 -2
  5. data/bin/publish +18 -0
  6. data/lib/osso/graphql/mutation.rb +7 -3
  7. data/lib/osso/graphql/mutations.rb +1 -3
  8. data/lib/osso/graphql/mutations/base_mutation.rb +18 -5
  9. data/lib/osso/graphql/mutations/configure_identity_provider.rb +8 -10
  10. data/lib/osso/graphql/mutations/create_enterprise_account.rb +2 -0
  11. data/lib/osso/graphql/mutations/create_identity_provider.rb +14 -5
  12. data/lib/osso/graphql/mutations/create_oauth_client.rb +1 -3
  13. data/lib/osso/graphql/mutations/delete_enterprise_account.rb +9 -11
  14. data/lib/osso/graphql/mutations/delete_oauth_client.rb +1 -3
  15. data/lib/osso/graphql/mutations/regenerate_oauth_credentials.rb +3 -5
  16. data/lib/osso/graphql/mutations/set_redirect_uris.rb +52 -0
  17. data/lib/osso/graphql/query.rb +7 -0
  18. data/lib/osso/graphql/resolvers.rb +1 -0
  19. data/lib/osso/graphql/resolvers/base_resolver.rb +21 -0
  20. data/lib/osso/graphql/resolvers/enterprise_account.rb +1 -11
  21. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
  22. data/lib/osso/graphql/resolvers/oauth_clients.rb +2 -2
  23. data/lib/osso/graphql/types.rb +2 -1
  24. data/lib/osso/graphql/types/admin_user.rb +22 -0
  25. data/lib/osso/graphql/types/base_object.rb +22 -0
  26. data/lib/osso/graphql/types/enterprise_account.rb +0 -5
  27. data/lib/osso/graphql/types/identity_provider.rb +0 -6
  28. data/lib/osso/graphql/types/oauth_client.rb +2 -4
  29. data/lib/osso/graphql/types/redirect_uri.rb +2 -4
  30. data/lib/osso/graphql/types/redirect_uri_input.rb +16 -0
  31. data/lib/osso/helpers/auth.rb +34 -15
  32. data/lib/osso/lib/route_map.rb +2 -2
  33. data/lib/osso/models/identity_provider.rb +6 -12
  34. data/lib/osso/models/oauth_client.rb +5 -0
  35. data/lib/osso/models/redirect_uri.rb +0 -11
  36. data/lib/osso/routes/admin.rb +2 -2
  37. data/lib/osso/routes/auth.rb +29 -12
  38. data/lib/osso/routes/oauth.rb +25 -18
  39. data/lib/osso/version.rb +1 -1
  40. data/spec/graphql/mutations/configure_identity_provider_spec.rb +17 -4
  41. data/spec/graphql/mutations/create_enterprise_account_spec.rb +13 -4
  42. data/spec/graphql/mutations/create_identity_provider_spec.rb +18 -6
  43. data/spec/graphql/mutations/create_oauth_client_spec.rb +10 -3
  44. data/spec/graphql/mutations/delete_enterprise_account_spec.rb +18 -4
  45. data/spec/graphql/mutations/delete_oauth_client_spec.rb +8 -4
  46. data/spec/graphql/query/enterprise_account_spec.rb +21 -6
  47. data/spec/graphql/query/enterprise_accounts_spec.rb +4 -2
  48. data/spec/graphql/query/identity_provider_spec.rb +16 -6
  49. data/spec/graphql/query/oauth_clients_spec.rb +10 -7
  50. data/spec/models/identity_provider_spec.rb +12 -0
  51. data/spec/routes/auth_spec.rb +18 -0
  52. data/spec/routes/oauth_spec.rb +5 -2
  53. data/spec/support/views/error.erb +0 -0
  54. metadata +12 -9
  55. data/lib/osso/graphql/mutations/add_redirect_uris_to_oauth_client.rb +0 -39
  56. data/lib/osso/graphql/mutations/delete_redirect_uri.rb +0 -38
  57. data/lib/osso/graphql/mutations/mark_redirect_uri_primary.rb +0 -34
  58. data/lib/osso/graphql/types/user.rb +0 -17
@@ -9,10 +9,11 @@ require_relative 'types/base_connection'
9
9
  require_relative 'types/base_object'
10
10
  require_relative 'types/base_enum'
11
11
  require_relative 'types/base_input_object'
12
+ require_relative 'types/admin_user'
12
13
  require_relative 'types/identity_provider_service'
13
14
  require_relative 'types/identity_provider_status'
14
15
  require_relative 'types/identity_provider'
15
16
  require_relative 'types/enterprise_account'
16
17
  require_relative 'types/redirect_uri'
18
+ require_relative 'types/redirect_uri_input'
17
19
  require_relative 'types/oauth_client'
18
- require_relative 'types/user'
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ module Osso
6
+ module GraphQL
7
+ module Types
8
+ class AdminUser < Types::BaseObject
9
+ description 'An Admin User of Osso'
10
+
11
+ field :id, ID, null: false
12
+ field :email, String, null: false
13
+ field :scope, String, null: false
14
+ field :oauth_client_id, ID, null: true
15
+
16
+ def self.authorized?(_object, _context)
17
+ true
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,6 +10,28 @@ module Osso
10
10
 
11
11
  field :created_at, ::GraphQL::Types::ISO8601DateTime, null: false
12
12
  field :updated_at, ::GraphQL::Types::ISO8601DateTime, null: false
13
+
14
+ def self.admin_authorized?(context)
15
+ context[:scope] == 'admin'
16
+ end
17
+
18
+ def self.internal_authorized?(context)
19
+ %w[admin internal].include?(context[:scope])
20
+ end
21
+
22
+ def self.enterprise_authorized?(context, domain)
23
+ return false unless domain
24
+
25
+ context[:email].split('@')[1] == domain
26
+ end
27
+
28
+ def self.authorized?(object, context)
29
+ # we first receive the payload object as a hash, but can depend on the
30
+ # return type to hide the actual objects non-admins shouldn't see
31
+ return true if object.class == Hash
32
+
33
+ internal_authorized?(context) || enterprise_authorized?(context, object&.domain)
34
+ end
13
35
  end
14
36
  end
15
37
  end
@@ -9,7 +9,6 @@ module Osso
9
9
  description 'An Account for a company that wishes to use SAML via Osso'
10
10
  implements ::GraphQL::Types::Relay::Node
11
11
 
12
- global_id_field :gid
13
12
  field :id, ID, null: false
14
13
  field :name, String, null: false
15
14
  field :domain, String, null: false
@@ -23,10 +22,6 @@ module Osso
23
22
  def identity_providers
24
23
  object.identity_providers
25
24
  end
26
-
27
- def self.authorized?(object, context)
28
- super && (context[:scope] == :admin || object.domain == context[:scope])
29
- end
30
25
  end
31
26
  end
32
27
  end
@@ -7,9 +7,7 @@ module Osso
7
7
  module Types
8
8
  class IdentityProvider < Types::BaseObject
9
9
  description 'Represents a SAML based IDP instance for an EnterpriseAccount'
10
- implements ::GraphQL::Types::Relay::Node
11
10
 
12
- global_id_field :gid
13
11
  field :id, ID, null: false
14
12
  field :enterprise_account_id, ID, null: false
15
13
  field :service, Types::IdentityProviderService, null: true
@@ -23,10 +21,6 @@ module Osso
23
21
  def documentation_pdf_url
24
22
  ENV['BASE_URL'] + '/identity_provider/documentation/' + @object.id
25
23
  end
26
-
27
- def self.authorized?(object, context)
28
- super && (context[:scope] == :admin || object.domain == context[:scope])
29
- end
30
24
  end
31
25
  end
32
26
  end
@@ -7,9 +7,7 @@ module Osso
7
7
  module Types
8
8
  class OauthClient < Types::BaseObject
9
9
  description 'An OAuth client used to consume Osso SAML users'
10
- implements ::GraphQL::Types::Relay::Node
11
10
 
12
- global_id_field :gid
13
11
  field :id, ID, null: false
14
12
  field :name, String, null: false
15
13
  field :client_id, String, null: false
@@ -24,8 +22,8 @@ module Osso
24
22
  object.secret
25
23
  end
26
24
 
27
- def self.authorized?(object, context)
28
- super && context[:scope] == :admin
25
+ def self.authorized?(_object, context)
26
+ admin_authorized?(context)
29
27
  end
30
28
  end
31
29
  end
@@ -7,15 +7,13 @@ module Osso
7
7
  module Types
8
8
  class RedirectUri < Types::BaseObject
9
9
  description 'An allowed redirect URI for an OauthClient'
10
- implements ::GraphQL::Types::Relay::Node
11
10
 
12
- global_id_field :gid
13
11
  field :id, ID, null: false
14
12
  field :uri, String, null: false
15
13
  field :primary, Boolean, null: false
16
14
 
17
- def self.authorized?(object, context)
18
- super && context[:scope] == :admin
15
+ def self.authorized?(_object, context)
16
+ context[:scope] == 'admin'
19
17
  end
20
18
  end
21
19
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ module Osso
6
+ module GraphQL
7
+ module Types
8
+ class RedirectUrisInput < Types::BaseInputObject
9
+ description 'Attributes for creating or updating a collection of redirect URIs for an Oauth Client'
10
+ argument :id, ID, 'Database ID', required: false
11
+ argument :uri, String, 'URI value', required: true
12
+ argument :primary, Boolean, 'Whether the URI is the primary uri used in IDP initiated login', required: true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,12 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pry'
3
4
  module Osso
4
5
  module Helpers
5
6
  module Auth
6
- attr_accessor :current_scope
7
+ END_USER_SCOPE = 'end-user'
8
+ INTERNAL_SCOPE = 'internal'
9
+ ADMIN_SCOPE = 'admin'
10
+
11
+ attr_accessor :current_user
12
+
13
+ def token_protected!
14
+ decode(token)
15
+ end
7
16
 
8
17
  def enterprise_protected!(domain = nil)
9
18
  return if admin_authorized?
19
+ return if internal_authorized?
10
20
  return if enterprise_authorized?(domain)
11
21
 
12
22
  halt 401 if request.post?
@@ -14,14 +24,26 @@ module Osso
14
24
  redirect ENV['JWT_URL']
15
25
  end
16
26
 
17
- # use client id in payload to restrict customer
18
- # users from accessing dev?
19
- def enterprise_authorized?(_domain)
20
- payload, _args = decode(token)
27
+ def enterprise_authorized?(domain)
28
+ decode(token)
29
+
30
+ @current_user[:scope] == END_USER_SCOPE &&
31
+ @current_user[:email].split('@')[1] == domain
32
+ rescue JWT::DecodeError
33
+ false
34
+ end
21
35
 
22
- @current_scope = payload['scope']
36
+ def internal_protected!
37
+ return if admin_authorized?
38
+ return if internal_authorized?
23
39
 
24
- true
40
+ redirect ENV['JWT_URL']
41
+ end
42
+
43
+ def internal_authorized?
44
+ decode(token)
45
+
46
+ @current_user[:scope] == INTERNAL_SCOPE
25
47
  rescue JWT::DecodeError
26
48
  false
27
49
  end
@@ -33,14 +55,9 @@ module Osso
33
55
  end
34
56
 
35
57
  def admin_authorized?
36
- payload, _args = decode(token)
58
+ decode(token)
37
59
 
38
- if payload['scope'] == 'admin'
39
- @current_scope = :admin
40
- return true
41
- end
42
-
43
- false
60
+ @current_user[:scope] == ADMIN_SCOPE
44
61
  rescue JWT::DecodeError
45
62
  false
46
63
  end
@@ -60,12 +77,14 @@ module Osso
60
77
  end
61
78
 
62
79
  def decode(token)
63
- JWT.decode(
80
+ payload, _args = JWT.decode(
64
81
  token,
65
82
  ENV['JWT_HMAC_SECRET'],
66
83
  true,
67
84
  { algorithm: 'HS256' },
68
85
  )
86
+
87
+ @current_user = payload.symbolize_keys
69
88
  end
70
89
  end
71
90
  end
@@ -11,12 +11,12 @@ module Osso
11
11
  use Osso::Oauth
12
12
 
13
13
  post '/graphql' do
14
- enterprise_protected!
14
+ token_protected!
15
15
 
16
16
  result = Osso::GraphQL::Schema.execute(
17
17
  params[:query],
18
18
  variables: params[:variables],
19
- context: { scope: current_scope },
19
+ context: current_user.symbolize_keys,
20
20
  )
21
21
 
22
22
  json result
@@ -19,20 +19,14 @@ module Osso
19
19
  end
20
20
 
21
21
  def saml_options
22
- attributes.slice(
23
- 'domain',
24
- 'idp_cert',
25
- 'idp_sso_target_url',
26
- ).symbolize_keys
22
+ {
23
+ domain: domain,
24
+ idp_sso_target_url: sso_url,
25
+ idp_cert: sso_cert,
26
+ issuer: domain,
27
+ }
27
28
  end
28
29
 
29
- # def saml_options
30
- # raise(
31
- # NoMethodError,
32
- # '#saml_options must be defined on each provider specific subclass',
33
- # )
34
- # end
35
-
36
30
  def assertion_consumer_service_url
37
31
  [
38
32
  ENV.fetch('BASE_URL'),
@@ -5,6 +5,7 @@ module Osso
5
5
  module Models
6
6
  class OauthClient < ActiveRecord::Base
7
7
  has_many :access_tokens
8
+ has_many :enterprise_accounts
8
9
  has_many :refresh_tokens
9
10
  has_many :identity_providers
10
11
  has_many :redirect_uris
@@ -17,6 +18,10 @@ module Osso
17
18
  redirect_uris.find(&:primary)
18
19
  end
19
20
 
21
+ def redirect_uri_values
22
+ redirect_uris.map(&:uri)
23
+ end
24
+
20
25
  def generate_secrets
21
26
  self.identifier = SecureRandom.hex(16)
22
27
  self.secret = SecureRandom.hex(32)
@@ -4,17 +4,6 @@ module Osso
4
4
  module Models
5
5
  class RedirectUri < ActiveRecord::Base
6
6
  belongs_to :oauth_client
7
-
8
- # TODO
9
- # before_validation :set_primary, on: :creaet, :update
10
-
11
- private
12
-
13
- def set_primary
14
- if primary_was.true? && primary.false?
15
-
16
- end
17
- end
18
7
  end
19
8
  end
20
9
  end
@@ -14,13 +14,13 @@ module Osso
14
14
 
15
15
  namespace '/admin' do
16
16
  get '' do
17
- admin_protected!
17
+ internal_protected!
18
18
 
19
19
  erb :admin
20
20
  end
21
21
 
22
22
  get '/enterprise' do
23
- admin_protected!
23
+ internal_protected!
24
24
 
25
25
  erb :admin
26
26
  end
@@ -14,10 +14,6 @@ module Osso
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
- def self.internal_redirect?(env)
18
- env['HTTP_REFERER']&.match(env['SERVER_NAME'])
19
- end
20
-
21
17
  use OmniAuth::Builder do
22
18
  OmniAuth::MultiProvider.register(
23
19
  self,
@@ -26,21 +22,24 @@ module Osso
26
22
  path_prefix: '/auth/saml',
27
23
  callback_suffix: 'callback',
28
24
  ) do |identity_provider_id, _env|
29
- provider = Models::IdentityProvider.find(identity_provider_id)
30
- provider.saml_options
25
+ Models::IdentityProvider.find(identity_provider_id).
26
+ saml_options
31
27
  end
32
28
  end
33
29
 
34
- namespace '/auth' do
30
+ namespace '/auth' do # rubocop:disable Metrics/BlockLength
31
+ get '/failure' do
32
+ @error = params[:message]
33
+ erb :error
34
+ end
35
35
  # Enterprise users are sent here after authenticating against
36
36
  # their Identity Provider. We find or create a user record,
37
37
  # and then create an authorization code for that user. The user
38
38
  # is redirected back to your application with this code
39
- # as a URL query param, which you then exhange for an access token
39
+ # as a URL query param, which you then exchange for an access token.
40
40
  post '/saml/:id/callback' do
41
41
  provider = Models::IdentityProvider.find(params[:id])
42
- oauth_client = provider.oauth_client
43
- redirect_uri = env['redirect_uri'] || oauth_client.default_redirect_uri.uri
42
+ @oauth_client = provider.oauth_client
44
43
 
45
44
  attributes = env['omniauth.auth']&.
46
45
  extra&.
@@ -56,11 +55,29 @@ module Osso
56
55
  end
57
56
 
58
57
  authorization_code = user.authorization_codes.create!(
59
- oauth_client: oauth_client,
58
+ oauth_client: @oauth_client,
60
59
  redirect_uri: redirect_uri,
61
60
  )
62
61
 
63
- redirect(redirect_uri + "?code=#{CGI.escape(authorization_code.token)}&state=#{session[:oauth_state]}")
62
+ # Mark IDP as 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
+
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]
64
81
  end
65
82
  end
66
83
  end
@@ -6,38 +6,45 @@ module Osso
6
6
  class Oauth < Sinatra::Base
7
7
  include AppConfig
8
8
  register Sinatra::Namespace
9
- # rubocop:disable Metrics/BlockLength
10
- namespace '/oauth' do
9
+
10
+ namespace '/oauth' do # rubocop:disable Metrics/BlockLength
11
11
  # Send your users here in order to being an authentication
12
12
  # flow. This flow follows the authorization grant oauth
13
13
  # spec with one exception - you must also pass the domain
14
- # of the user who wants to sign in.
14
+ # of the user who wants to sign in. If the sign in request
15
+ # is valid, the user is redirected to their Identity Provider.
16
+ # Once they complete IdP login, they will be returned to the
17
+ # redirect_uri with an authorization code parameter.
15
18
  get '/authorize' do
16
- @enterprise = Models::EnterpriseAccount.
17
- includes(:identity_providers).
18
- find_by!(domain: params[:domain])
19
-
20
19
  Rack::OAuth2::Server::Authorize.new do |req, _res|
21
20
  client = Models::OauthClient.find_by!(identifier: req.client_id)
22
- req.verify_redirect_uri!(client.redirect_uri_values)
21
+ session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
22
+ session[:osso_oauth_state] = params[:state]
23
23
  end.call(env)
24
24
 
25
- if @enterprise.single_provider?
26
- session[:oauth_state] = params[:state]
27
- redirect "/auth/saml/#{@enterprise.provider.id}"
28
- end
25
+ enterprise = Models::EnterpriseAccount.
26
+ includes(:identity_providers).
27
+ find_by!(domain: params[:domain])
28
+
29
+ redirect "/auth/saml/#{enterprise.provider.id}" if enterprise.single_provider?
29
30
 
30
31
  # TODO: multiple provider support
31
32
  # erb :multiple_providers
32
33
 
33
34
  rescue Rack::OAuth2::Server::Authorize::BadRequest => e
34
35
  @error = e
35
- return erb :error
36
+ erb :error
37
+ rescue ActiveRecord::RecordNotFound => e
38
+ @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
+ erb :error
36
42
  end
37
43
 
38
- # Exchange an authorization code token for an access token.
39
- # In addition to the token, you must include all paramaters
40
- # required by Oauth spec: redirect_uri, client ID, and client secret
44
+ # Exchange an authorization code for an access token.
45
+ # In addition to the authorization code, you must include all
46
+ # paramaters required by OAuth spec: redirect_uri, client ID,
47
+ # and client secret
41
48
  post '/token' do
42
49
  Rack::OAuth2::Server::Token.new do |req, res|
43
50
  code = Models::AuthorizationCode.
@@ -49,7 +56,8 @@ module Osso
49
56
  end.call(env)
50
57
  end
51
58
 
52
- # Use the access token to request a user profile
59
+ # Use the access token to request a profile for the user who
60
+ # just logged in. Access tokens are short-lived.
53
61
  get '/me' do
54
62
  json Models::AccessToken.
55
63
  includes(:user).
@@ -60,4 +68,3 @@ module Osso
60
68
  end
61
69
  end
62
70
  end
63
- # rubocop:enable Metrics/BlockLength