osso 0.0.3.13 → 0.0.3.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) 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 +6 -2
  7. data/lib/osso/graphql/mutations/base_mutation.rb +18 -5
  8. data/lib/osso/graphql/mutations/configure_identity_provider.rb +8 -10
  9. data/lib/osso/graphql/mutations/create_enterprise_account.rb +2 -0
  10. data/lib/osso/graphql/mutations/create_identity_provider.rb +14 -5
  11. data/lib/osso/graphql/mutations/create_oauth_client.rb +1 -3
  12. data/lib/osso/graphql/mutations/delete_enterprise_account.rb +9 -11
  13. data/lib/osso/graphql/mutations/delete_oauth_client.rb +1 -3
  14. data/lib/osso/graphql/mutations/regenerate_oauth_credentials.rb +1 -3
  15. data/lib/osso/graphql/mutations/set_redirect_uris.rb +2 -4
  16. data/lib/osso/graphql/query.rb +7 -0
  17. data/lib/osso/graphql/resolvers.rb +1 -0
  18. data/lib/osso/graphql/resolvers/base_resolver.rb +21 -0
  19. data/lib/osso/graphql/resolvers/enterprise_account.rb +1 -11
  20. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
  21. data/lib/osso/graphql/resolvers/oauth_clients.rb +2 -2
  22. data/lib/osso/graphql/types.rb +1 -1
  23. data/lib/osso/graphql/types/admin_user.rb +22 -0
  24. data/lib/osso/graphql/types/base_object.rb +22 -0
  25. data/lib/osso/graphql/types/enterprise_account.rb +0 -5
  26. data/lib/osso/graphql/types/identity_provider.rb +0 -6
  27. data/lib/osso/graphql/types/oauth_client.rb +2 -4
  28. data/lib/osso/graphql/types/redirect_uri.rb +2 -4
  29. data/lib/osso/helpers/auth.rb +35 -15
  30. data/lib/osso/lib/route_map.rb +2 -2
  31. data/lib/osso/models/identity_provider.rb +6 -12
  32. data/lib/osso/models/oauth_client.rb +1 -0
  33. data/lib/osso/models/redirect_uri.rb +0 -11
  34. data/lib/osso/routes/admin.rb +2 -2
  35. data/lib/osso/routes/auth.rb +29 -12
  36. data/lib/osso/routes/oauth.rb +25 -18
  37. data/lib/osso/version.rb +1 -1
  38. data/spec/graphql/mutations/configure_identity_provider_spec.rb +17 -4
  39. data/spec/graphql/mutations/create_enterprise_account_spec.rb +13 -4
  40. data/spec/graphql/mutations/create_identity_provider_spec.rb +18 -6
  41. data/spec/graphql/mutations/create_oauth_client_spec.rb +10 -3
  42. data/spec/graphql/mutations/delete_enterprise_account_spec.rb +18 -4
  43. data/spec/graphql/mutations/delete_oauth_client_spec.rb +8 -4
  44. data/spec/graphql/query/enterprise_account_spec.rb +21 -6
  45. data/spec/graphql/query/enterprise_accounts_spec.rb +4 -2
  46. data/spec/graphql/query/identity_provider_spec.rb +16 -6
  47. data/spec/graphql/query/oauth_clients_spec.rb +10 -7
  48. data/spec/models/identity_provider_spec.rb +12 -0
  49. data/spec/routes/auth_spec.rb +18 -0
  50. data/spec/routes/oauth_spec.rb +5 -2
  51. data/spec/support/views/error.erb +0 -0
  52. metadata +10 -6
  53. data/lib/osso/graphql/types/user.rb +0 -17
@@ -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
@@ -3,10 +3,21 @@
3
3
  module Osso
4
4
  module Helpers
5
5
  module Auth
6
- attr_accessor :current_scope
6
+ END_USER_SCOPE = 'end-user'
7
+ INTERNAL_SCOPE = 'internal'
8
+ ADMIN_SCOPE = 'admin'
9
+
10
+ attr_accessor :current_user
11
+
12
+ def token_protected!
13
+ decode(token)
14
+ rescue JWT::DecodeError
15
+ halt 401
16
+ end
7
17
 
8
18
  def enterprise_protected!(domain = nil)
9
19
  return if admin_authorized?
20
+ return if internal_authorized?
10
21
  return if enterprise_authorized?(domain)
11
22
 
12
23
  halt 401 if request.post?
@@ -14,14 +25,26 @@ module Osso
14
25
  redirect ENV['JWT_URL']
15
26
  end
16
27
 
17
- # use client id in payload to restrict customer
18
- # users from accessing dev?
19
- def enterprise_authorized?(_domain)
20
- payload, _args = decode(token)
28
+ def enterprise_authorized?(domain)
29
+ decode(token)
30
+
31
+ @current_user[:scope] == END_USER_SCOPE &&
32
+ @current_user[:email].split('@')[1] == domain
33
+ rescue JWT::DecodeError
34
+ false
35
+ end
36
+
37
+ def internal_protected!
38
+ return if admin_authorized?
39
+ return if internal_authorized?
40
+
41
+ redirect ENV['JWT_URL']
42
+ end
21
43
 
22
- @current_scope = payload['scope']
44
+ def internal_authorized?
45
+ decode(token)
23
46
 
24
- true
47
+ @current_user[:scope] == INTERNAL_SCOPE
25
48
  rescue JWT::DecodeError
26
49
  false
27
50
  end
@@ -33,14 +56,9 @@ module Osso
33
56
  end
34
57
 
35
58
  def admin_authorized?
36
- payload, _args = decode(token)
37
-
38
- if payload['scope'] == 'admin'
39
- @current_scope = :admin
40
- return true
41
- end
59
+ decode(token)
42
60
 
43
- false
61
+ @current_user[:scope] == ADMIN_SCOPE
44
62
  rescue JWT::DecodeError
45
63
  false
46
64
  end
@@ -60,12 +78,14 @@ module Osso
60
78
  end
61
79
 
62
80
  def decode(token)
63
- JWT.decode(
81
+ payload, _args = JWT.decode(
64
82
  token,
65
83
  ENV['JWT_HMAC_SECRET'],
66
84
  true,
67
85
  { algorithm: 'HS256' },
68
86
  )
87
+
88
+ @current_user = payload.symbolize_keys
69
89
  end
70
90
  end
71
91
  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
@@ -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.primary_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
- VERSION = '0.0.3.13'
4
+ VERSION = '0.0.3.18'
5
5
  end
@@ -39,12 +39,15 @@ describe Osso::GraphQL::Schema do
39
39
  described_class.execute(
40
40
  mutation,
41
41
  variables: variables,
42
- context: { scope: current_scope },
42
+ context: current_context,
43
43
  )
44
44
  end
45
45
 
46
46
  describe 'for an admin user' do
47
- let(:current_scope) { :admin }
47
+ let(:current_context) do
48
+ { scope: 'admin' }
49
+ end
50
+
48
51
  it 'configures an identity provider' do
49
52
  expect(subject.dig('data', 'configureIdentityProvider', 'identityProvider', 'status')).
50
53
  to eq('Configured')
@@ -53,7 +56,12 @@ describe Osso::GraphQL::Schema do
53
56
 
54
57
  describe 'for an email scoped user' do
55
58
  let(:domain) { Faker::Internet.domain_name }
56
- let(:current_scope) { domain }
59
+ let(:current_context) do
60
+ {
61
+ scope: 'end-user',
62
+ email: "user@#{domain}",
63
+ }
64
+ end
57
65
  let(:enterprise_account) { create(:enterprise_account, domain: domain) }
58
66
  let(:identity_provider) { create(:identity_provider, enterprise_account: enterprise_account, domain: domain) }
59
67
 
@@ -65,7 +73,12 @@ describe Osso::GraphQL::Schema do
65
73
 
66
74
  describe 'for the wrong email scoped user' do
67
75
  let(:domain) { Faker::Internet.domain_name }
68
- let(:current_scope) { domain }
76
+ let(:current_context) do
77
+ {
78
+ scope: 'end-user',
79
+ email: "user@#{domain}",
80
+ }
81
+ end
69
82
 
70
83
  it 'does not configure an identity provider' do
71
84
  expect(subject.dig('errors')).to_not be_empty