osso 0.0.3.12 → 0.0.3.17

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.
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