osso 0.0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.buildkite/hooks/environment +9 -0
- data/.buildkite/hooks/pre-command +7 -0
- data/.buildkite/pipeline.yml +6 -0
- data/.buildkite/template.yml +5 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +81 -0
- data/CODE_OF_CONDUCT.md +130 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +176 -0
- data/LICENSE +111 -0
- data/README.md +2 -0
- data/Rakefile +14 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/config/database.yml +14 -0
- data/db/schema.rb +133 -0
- data/lib/osso.rb +11 -0
- data/lib/osso/Rakefile +13 -0
- data/lib/osso/db/migrate/20190909230109_enable_uuid.rb +7 -0
- data/lib/osso/db/migrate/20200328135750_create_users.rb +12 -0
- data/lib/osso/db/migrate/20200328143303_create_oauth_tables.rb +57 -0
- data/lib/osso/db/migrate/20200328143305_create_identity_providers.rb +12 -0
- data/lib/osso/db/migrate/20200411184535_add_provider_id_to_users.rb +7 -0
- data/lib/osso/db/migrate/20200411192645_create_enterprise_accounts.rb +15 -0
- data/lib/osso/db/migrate/20200413132407_add_oauth_clients.rb +13 -0
- data/lib/osso/db/migrate/20200413142511_create_authorization_codes.rb +15 -0
- data/lib/osso/db/migrate/20200413163451_create_access_tokens.rb +13 -0
- data/lib/osso/db/migrate/20200502120616_create_redirect_uris_and_drop_from_oauth_clients.rb +13 -0
- data/lib/osso/db/migrate/20200502135008_add_oauth_client_id_to_enterprise_accounts_and_identity_providers.rb +6 -0
- data/lib/osso/db/migrate/20200714223226_add_identity_provider_service_enum.rb +17 -0
- data/lib/osso/db/migrate/20200715154211_rename_idp_fields_on_identity_provider_to_sso.rb +6 -0
- data/lib/osso/db/migrate/20200715205801_add_name_to_enterprise_account.rb +5 -0
- data/lib/osso/graphql/mutation.rb +16 -0
- data/lib/osso/graphql/mutations.rb +12 -0
- data/lib/osso/graphql/mutations/base_mutation.rb +41 -0
- data/lib/osso/graphql/mutations/configure_identity_provider.rb +36 -0
- data/lib/osso/graphql/mutations/create_enterprise_account.rb +25 -0
- data/lib/osso/graphql/mutations/create_identity_provider.rb +30 -0
- data/lib/osso/graphql/mutations/set_identity_provider.rb +27 -0
- data/lib/osso/graphql/query.rb +25 -0
- data/lib/osso/graphql/resolvers.rb +12 -0
- data/lib/osso/graphql/resolvers/enterprise_account.rb +25 -0
- data/lib/osso/graphql/resolvers/enterprise_accounts.rb +17 -0
- data/lib/osso/graphql/resolvers/oauth_clients.rb +15 -0
- data/lib/osso/graphql/schema.rb +46 -0
- data/lib/osso/graphql/types.rb +15 -0
- data/lib/osso/graphql/types/base_enum.rb +10 -0
- data/lib/osso/graphql/types/base_input_object.rb +10 -0
- data/lib/osso/graphql/types/base_object.rb +12 -0
- data/lib/osso/graphql/types/enterprise_account.rb +33 -0
- data/lib/osso/graphql/types/identity_provider.rb +37 -0
- data/lib/osso/graphql/types/identity_provider_service.rb +12 -0
- data/lib/osso/graphql/types/oauth_client.rb +20 -0
- data/lib/osso/graphql/types/user.rb +17 -0
- data/lib/osso/helpers/auth.rb +71 -0
- data/lib/osso/helpers/helpers.rb +8 -0
- data/lib/osso/lib/app_config.rb +20 -0
- data/lib/osso/lib/oauth2_token.rb +38 -0
- data/lib/osso/lib/route_map.rb +28 -0
- data/lib/osso/models/access_token.rb +29 -0
- data/lib/osso/models/authorization_code.rb +14 -0
- data/lib/osso/models/enterprise_account.rb +28 -0
- data/lib/osso/models/identity_provider.rb +48 -0
- data/lib/osso/models/models.rb +16 -0
- data/lib/osso/models/oauth_client.rb +32 -0
- data/lib/osso/models/redirect_uri.rb +20 -0
- data/lib/osso/models/saml_provider.rb +49 -0
- data/lib/osso/models/saml_providers/azure_saml_provider.rb +22 -0
- data/lib/osso/models/saml_providers/okta_saml_provider.rb +23 -0
- data/lib/osso/models/user.rb +24 -0
- data/lib/osso/rake.rb +4 -0
- data/lib/osso/routes/admin.rb +41 -0
- data/lib/osso/routes/auth.rb +67 -0
- data/lib/osso/routes/oauth.rb +63 -0
- data/lib/osso/routes/routes.rb +10 -0
- data/lib/osso/routes/views/error.erb +1 -0
- data/lib/osso/routes/views/multiple_providers.erb +1 -0
- data/lib/osso/version.rb +5 -0
- data/lib/tasks/bootstrap.rake +16 -0
- data/osso-rb.gemspec +40 -0
- data/spec/factories/authorization_code.rb +10 -0
- data/spec/factories/enterprise_account.rb +46 -0
- data/spec/factories/identity_providers.rb +49 -0
- data/spec/factories/oauth_client.rb +12 -0
- data/spec/factories/redirect_uri.rb +14 -0
- data/spec/factories/user.rb +18 -0
- data/spec/graphql/mutations/configure_identity_provider_spec.rb +75 -0
- data/spec/graphql/mutations/create_enterprise_account_spec.rb +68 -0
- data/spec/graphql/mutations/create_identity_provider_spec.rb +104 -0
- data/spec/graphql/query/enterprise_account_spec.rb +68 -0
- data/spec/graphql/query/enterprise_accounts_spec.rb +44 -0
- data/spec/graphql/query/identity_provider_spec.rb +65 -0
- data/spec/graphql/query/oauth_clients_account_spec.rb +48 -0
- data/spec/models/azure_saml_provider_spec.rb +19 -0
- data/spec/models/identity_provider_spec.rb +17 -0
- data/spec/models/okta_saml_provider_spec.rb +20 -0
- data/spec/routes/admin_spec.rb +60 -0
- data/spec/routes/app_spec.rb +6 -0
- data/spec/routes/auth_spec.rb +112 -0
- data/spec/routes/oauth_spec.rb +134 -0
- data/spec/spec_helper.rb +68 -0
- data/spec/support/spec_app.rb +9 -0
- data/spec/support/views/admin.erb +5 -0
- metadata +348 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Types
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative 'types/base_object'
|
9
|
+
require_relative 'types/base_enum'
|
10
|
+
require_relative 'types/base_input_object'
|
11
|
+
require_relative 'types/identity_provider_service'
|
12
|
+
require_relative 'types/identity_provider'
|
13
|
+
require_relative 'types/enterprise_account'
|
14
|
+
require_relative 'types/oauth_client'
|
15
|
+
require_relative 'types/user'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
module Osso
|
6
|
+
module GraphQL
|
7
|
+
module Types
|
8
|
+
class EnterpriseAccount < Types::BaseObject
|
9
|
+
description 'An Account for a company that wishes to use SAML via Osso'
|
10
|
+
implements ::GraphQL::Types::Relay::Node
|
11
|
+
|
12
|
+
global_id_field :gid
|
13
|
+
field :id, ID, null: false
|
14
|
+
field :name, String, null: false
|
15
|
+
field :domain, String, null: false
|
16
|
+
field :identity_providers, [Types::IdentityProvider], null: true
|
17
|
+
field :status, String, null: false
|
18
|
+
|
19
|
+
def status
|
20
|
+
'active'
|
21
|
+
end
|
22
|
+
|
23
|
+
def identity_providers
|
24
|
+
object.identity_providers
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.authorized?(object, context)
|
28
|
+
super && (context[:scope] == :admin || object.domain == context[:scope])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
module Osso
|
6
|
+
module GraphQL
|
7
|
+
module Types
|
8
|
+
class IdentityProvider < Types::BaseObject
|
9
|
+
description 'Represents a SAML based IDP instance for an EnterpriseAccount'
|
10
|
+
implements ::GraphQL::Types::Relay::Node
|
11
|
+
|
12
|
+
global_id_field :gid
|
13
|
+
field :id, ID, null: false
|
14
|
+
field :enterprise_account_id, ID, null: false
|
15
|
+
field :service, Types::IdentityProviderService, null: true
|
16
|
+
field :domain, String, null: false
|
17
|
+
field :acs_url, String, null: false
|
18
|
+
field :sso_url, String, null: true
|
19
|
+
field :sso_cert, String, null: true
|
20
|
+
field :configured, Boolean, null: false
|
21
|
+
field :documentation_pdf_url, String, null: true
|
22
|
+
|
23
|
+
def configured
|
24
|
+
!!(@object.sso_url && @object.sso_cert)
|
25
|
+
end
|
26
|
+
|
27
|
+
def documentation_pdf_url
|
28
|
+
ENV['BASE_URL'] + '/identity_provider/documentation/' + @object.id
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.authorized?(object, context)
|
32
|
+
super && (context[:scope] == :admin || object.domain == context[:scope])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module GraphQL
|
5
|
+
module Types
|
6
|
+
class IdentityProviderService < BaseEnum
|
7
|
+
value('AZURE', 'Microsoft Azure Identity Provider', value: 'Osso::Models::AzureSamlProvider')
|
8
|
+
value('OKTA', 'Okta Identity Provider', value: 'Osso::Models::OktaSamlProvider')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
module Osso
|
6
|
+
module GraphQL
|
7
|
+
module Types
|
8
|
+
class OAuthClient < Types::BaseObject
|
9
|
+
description 'An OAuth client used to consume Osso SAML users'
|
10
|
+
implements ::GraphQL::Types::Relay::Node
|
11
|
+
|
12
|
+
global_id_field :gid
|
13
|
+
field :id, ID, null: false
|
14
|
+
field :name, String, null: false
|
15
|
+
field :client_id, String, null: false
|
16
|
+
field :client_secret, String, null: false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
require_relative 'base_object'
|
5
|
+
|
6
|
+
module Osso
|
7
|
+
module GraphQL
|
8
|
+
module Types
|
9
|
+
class User < Types::BaseObject
|
10
|
+
description 'A User of the application'
|
11
|
+
|
12
|
+
field :id, ID, null: false
|
13
|
+
field :name, String, null: true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Helpers
|
5
|
+
module Auth
|
6
|
+
attr_accessor :current_scope
|
7
|
+
|
8
|
+
def enterprise_protected!(domain = nil)
|
9
|
+
return if admin_authorized?
|
10
|
+
return if enterprise_authorized?(domain)
|
11
|
+
|
12
|
+
halt 401 if request.post?
|
13
|
+
|
14
|
+
redirect ENV['JWT_URL']
|
15
|
+
end
|
16
|
+
|
17
|
+
def enterprise_authorized?(_domain)
|
18
|
+
payload, _args = JWT.decode(
|
19
|
+
token,
|
20
|
+
ENV['JWT_HMAC_SECRET'],
|
21
|
+
true,
|
22
|
+
{ algorithm: 'HS256' },
|
23
|
+
)
|
24
|
+
|
25
|
+
@current_scope = payload['scope']
|
26
|
+
|
27
|
+
true
|
28
|
+
rescue JWT::DecodeError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def admin_protected!
|
33
|
+
return if admin_authorized?
|
34
|
+
|
35
|
+
redirect ENV['JWT_URL']
|
36
|
+
end
|
37
|
+
|
38
|
+
def admin_authorized?
|
39
|
+
payload, _args = JWT.decode(
|
40
|
+
token,
|
41
|
+
ENV['JWT_HMAC_SECRET'],
|
42
|
+
true,
|
43
|
+
{ algorithm: 'HS256' },
|
44
|
+
)
|
45
|
+
|
46
|
+
if payload['scope'] == 'admin'
|
47
|
+
@current_scope = :admin
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
|
51
|
+
false
|
52
|
+
rescue JWT::DecodeError
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def token
|
57
|
+
request.env['admin_token'] || session['admin_token'] || request['admin_token']
|
58
|
+
end
|
59
|
+
|
60
|
+
def chomp_token
|
61
|
+
return unless request['admin_token'].present?
|
62
|
+
|
63
|
+
session['admin_token'] = request['admin_token']
|
64
|
+
|
65
|
+
return if request.post?
|
66
|
+
|
67
|
+
redirect request.path
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/contrib'
|
4
|
+
|
5
|
+
module Osso
|
6
|
+
module AppConfig
|
7
|
+
def self.included(klass)
|
8
|
+
klass.class_eval do
|
9
|
+
use Rack::JSONBodyParser
|
10
|
+
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
11
|
+
|
12
|
+
error ActiveRecord::RecordNotFound do
|
13
|
+
status 404
|
14
|
+
end
|
15
|
+
|
16
|
+
set :root, Dir.pwd
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Osso
|
6
|
+
module OAuth2Token
|
7
|
+
def self.included(klass)
|
8
|
+
klass.class_eval do
|
9
|
+
cattr_accessor :default_lifetime
|
10
|
+
self.default_lifetime = 1.minute
|
11
|
+
belongs_to :user
|
12
|
+
belongs_to :oauth_client
|
13
|
+
|
14
|
+
before_validation :setup, on: :create
|
15
|
+
validates :oauth_client, :expires_at, presence: true
|
16
|
+
validates :token, presence: true, uniqueness: true
|
17
|
+
|
18
|
+
scope :valid, -> { where('expires_at > ?', Time.now.utc) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def expires_in
|
23
|
+
(expires_at - Time.now.utc).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def expired!
|
27
|
+
self.expires_at = Time.now.utc
|
28
|
+
save!
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def setup
|
34
|
+
self.token = SecureRandom.hex(32)
|
35
|
+
self.expires_at ||= default_lifetime.from_now
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/MethodLength
|
4
|
+
|
5
|
+
module Osso
|
6
|
+
module RouteMap
|
7
|
+
def self.included(klass)
|
8
|
+
klass.class_eval do
|
9
|
+
use Osso::Admin
|
10
|
+
use Osso::Auth
|
11
|
+
use Osso::Oauth
|
12
|
+
|
13
|
+
post '/graphql' do
|
14
|
+
enterprise_protected!
|
15
|
+
|
16
|
+
result = Osso::GraphQL::Schema.execute(
|
17
|
+
params[:query],
|
18
|
+
variables: params[:variables],
|
19
|
+
context: { scope: current_scope },
|
20
|
+
)
|
21
|
+
|
22
|
+
json result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
# rubocop:enable Metrics/MethodLength
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Models
|
5
|
+
class AccessToken < ::ActiveRecord::Base
|
6
|
+
include OAuth2Token
|
7
|
+
self.default_lifetime = 10.minutes
|
8
|
+
belongs_to :refresh_token
|
9
|
+
|
10
|
+
def to_bearer_token
|
11
|
+
Rack::OAuth2::AccessToken::Bearer.new(
|
12
|
+
access_token: token,
|
13
|
+
expires_in: expires_in,
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def setup
|
20
|
+
super
|
21
|
+
return unless refresh_token
|
22
|
+
|
23
|
+
self.user = refresh_token.user
|
24
|
+
self.client = refresh_token.client
|
25
|
+
self.expires_at = [expires_at, refresh_token.expires_at].min
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Models
|
5
|
+
class AuthorizationCode < ActiveRecord::Base
|
6
|
+
include OAuth2Token
|
7
|
+
|
8
|
+
def access_token
|
9
|
+
@access_token ||= expired! &&
|
10
|
+
user.access_tokens.create(oauth_client: oauth_client)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Models
|
5
|
+
# Base class for Enterprises. This should map one-to-one with
|
6
|
+
# your own Account model. Persisting the EnterpriseAccount id
|
7
|
+
# in your application's database is recommended. The table also
|
8
|
+
# includes fields for external IDs such that you can persist
|
9
|
+
# your ID for an account in your Osso instance.
|
10
|
+
class EnterpriseAccount < ActiveRecord::Base
|
11
|
+
belongs_to :oauth_client
|
12
|
+
has_many :users
|
13
|
+
has_many :identity_providers
|
14
|
+
|
15
|
+
def single_provider?
|
16
|
+
identity_providers.one?
|
17
|
+
end
|
18
|
+
|
19
|
+
def provider
|
20
|
+
return nil unless single_provider?
|
21
|
+
|
22
|
+
identity_providers.first
|
23
|
+
end
|
24
|
+
|
25
|
+
alias identity_provider provider
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Models
|
5
|
+
# Base class for SAML Providers
|
6
|
+
class IdentityProvider < ActiveRecord::Base
|
7
|
+
NAME_FORMAT = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
8
|
+
belongs_to :enterprise_account
|
9
|
+
belongs_to :oauth_client
|
10
|
+
has_many :users
|
11
|
+
|
12
|
+
def name
|
13
|
+
service.titlecase
|
14
|
+
# raise(
|
15
|
+
# NoMethodError,
|
16
|
+
# '#name must be defined on each provider specific subclass',
|
17
|
+
# )
|
18
|
+
end
|
19
|
+
|
20
|
+
def saml_options
|
21
|
+
attributes.slice(
|
22
|
+
'domain',
|
23
|
+
'idp_cert',
|
24
|
+
'idp_sso_target_url',
|
25
|
+
).symbolize_keys
|
26
|
+
end
|
27
|
+
|
28
|
+
# def saml_options
|
29
|
+
# raise(
|
30
|
+
# NoMethodError,
|
31
|
+
# '#saml_options must be defined on each provider specific subclass',
|
32
|
+
# )
|
33
|
+
# end
|
34
|
+
|
35
|
+
def assertion_consumer_service_url
|
36
|
+
[
|
37
|
+
ENV.fetch('BASE_URL'),
|
38
|
+
'auth',
|
39
|
+
'saml',
|
40
|
+
id,
|
41
|
+
'callback',
|
42
|
+
].join('/')
|
43
|
+
end
|
44
|
+
|
45
|
+
alias acs_url assertion_consumer_service_url
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|