descope 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yaml +54 -0
- data/.gitignore +59 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +10 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +90 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +204 -0
- data/LICENSE +21 -0
- data/README.md +1171 -0
- data/Rakefile +31 -0
- data/descope.gemspec +34 -0
- data/examples/ruby/Gemfile +4 -0
- data/examples/ruby/Gemfile.lock +41 -0
- data/examples/ruby/access_key_app.rb +45 -0
- data/examples/ruby/enchantedlink_app.rb +65 -0
- data/examples/ruby/magiclink_app.rb +81 -0
- data/examples/ruby/management/Gemfile +5 -0
- data/examples/ruby/management/Gemfile.lock +38 -0
- data/examples/ruby/management/access_key_app.rb +71 -0
- data/examples/ruby/management/audit_app.rb +25 -0
- data/examples/ruby/management/authz_app.rb +135 -0
- data/examples/ruby/management/authz_files.json +229 -0
- data/examples/ruby/management/flow_app.rb +57 -0
- data/examples/ruby/management/permission_app.rb +56 -0
- data/examples/ruby/management/role_app.rb +58 -0
- data/examples/ruby/management/tenant_app.rb +60 -0
- data/examples/ruby/management/user_app.rb +60 -0
- data/examples/ruby/oauth_app.rb +39 -0
- data/examples/ruby/otp_app.rb +50 -0
- data/examples/ruby/password_app.rb +76 -0
- data/examples/ruby/saml_app.rb +38 -0
- data/examples/ruby-on-rails-api/descope/.dockerignore +37 -0
- data/examples/ruby-on-rails-api/descope/.gitattributes +9 -0
- data/examples/ruby-on-rails-api/descope/.gitignore +40 -0
- data/examples/ruby-on-rails-api/descope/.node-version +1 -0
- data/examples/ruby-on-rails-api/descope/.ruby-version +1 -0
- data/examples/ruby-on-rails-api/descope/Dockerfile +75 -0
- data/examples/ruby-on-rails-api/descope/Gemfile +67 -0
- data/examples/ruby-on-rails-api/descope/Gemfile.lock +284 -0
- data/examples/ruby-on-rails-api/descope/Procfile.dev +3 -0
- data/examples/ruby-on-rails-api/descope/README.md +54 -0
- data/examples/ruby-on-rails-api/descope/Rakefile +6 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/app/assets/config/manifest.js +3 -0
- data/examples/ruby-on-rails-api/descope/app/assets/images/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/app/assets/images/descope.jpeg +0 -0
- data/examples/ruby-on-rails-api/descope/app/assets/images/favicon.ico +0 -0
- data/examples/ruby-on-rails-api/descope/app/assets/images/logo192.png +0 -0
- data/examples/ruby-on-rails-api/descope/app/assets/images/logo512.png +0 -0
- data/examples/ruby-on-rails-api/descope/app/assets/stylesheets/application.bootstrap.scss +67 -0
- data/examples/ruby-on-rails-api/descope/app/channels/application_cable/channel.rb +4 -0
- data/examples/ruby-on-rails-api/descope/app/channels/application_cable/connection.rb +4 -0
- data/examples/ruby-on-rails-api/descope/app/controllers/application_controller.rb +2 -0
- data/examples/ruby-on-rails-api/descope/app/controllers/concerns/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/app/controllers/homepage_controller.rb +4 -0
- data/examples/ruby-on-rails-api/descope/app/controllers/session_controller.rb +66 -0
- data/examples/ruby-on-rails-api/descope/app/helpers/application_helper.rb +2 -0
- data/examples/ruby-on-rails-api/descope/app/helpers/homepage_helper.rb +2 -0
- data/examples/ruby-on-rails-api/descope/app/helpers/session_helper.rb +2 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/App.css +53 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/application.js +5 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/App.jsx +4 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/Dashboard.jsx +60 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/Home.jsx +27 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/Login.jsx +45 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/Profile.jsx +81 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/index.html +11 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/components/index.jsx +24 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/controllers/application.js +9 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/controllers/index.js +5 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/reportWebVitals.js +13 -0
- data/examples/ruby-on-rails-api/descope/app/javascript/routes/index.jsx +17 -0
- data/examples/ruby-on-rails-api/descope/app/jobs/application_job.rb +7 -0
- data/examples/ruby-on-rails-api/descope/app/mailers/application_mailer.rb +4 -0
- data/examples/ruby-on-rails-api/descope/app/models/application_record.rb +3 -0
- data/examples/ruby-on-rails-api/descope/app/models/concerns/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/app/views/homepage/index.html.erb +2 -0
- data/examples/ruby-on-rails-api/descope/app/views/layouts/application.html.erb +16 -0
- data/examples/ruby-on-rails-api/descope/app/views/layouts/mailer.html.erb +13 -0
- data/examples/ruby-on-rails-api/descope/app/views/layouts/mailer.text.erb +1 -0
- data/examples/ruby-on-rails-api/descope/app/views/session/index.html.erb +2 -0
- data/examples/ruby-on-rails-api/descope/bin/bundle +109 -0
- data/examples/ruby-on-rails-api/descope/bin/dev +11 -0
- data/examples/ruby-on-rails-api/descope/bin/docker-entrypoint +8 -0
- data/examples/ruby-on-rails-api/descope/bin/rails +4 -0
- data/examples/ruby-on-rails-api/descope/bin/rake +4 -0
- data/examples/ruby-on-rails-api/descope/bin/setup +36 -0
- data/examples/ruby-on-rails-api/descope/build.js +30 -0
- data/examples/ruby-on-rails-api/descope/config/application.rb +42 -0
- data/examples/ruby-on-rails-api/descope/config/boot.rb +4 -0
- data/examples/ruby-on-rails-api/descope/config/cable.yml +10 -0
- data/examples/ruby-on-rails-api/descope/config/config.yml +9 -0
- data/examples/ruby-on-rails-api/descope/config/credentials.yml.enc +1 -0
- data/examples/ruby-on-rails-api/descope/config/database.yml +25 -0
- data/examples/ruby-on-rails-api/descope/config/environment.rb +5 -0
- data/examples/ruby-on-rails-api/descope/config/environments/development.rb +76 -0
- data/examples/ruby-on-rails-api/descope/config/environments/production.rb +97 -0
- data/examples/ruby-on-rails-api/descope/config/environments/test.rb +64 -0
- data/examples/ruby-on-rails-api/descope/config/initializers/assets.rb +13 -0
- data/examples/ruby-on-rails-api/descope/config/initializers/content_security_policy.rb +25 -0
- data/examples/ruby-on-rails-api/descope/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/ruby-on-rails-api/descope/config/initializers/inflections.rb +16 -0
- data/examples/ruby-on-rails-api/descope/config/initializers/load_config.rb +12 -0
- data/examples/ruby-on-rails-api/descope/config/initializers/permissions_policy.rb +13 -0
- data/examples/ruby-on-rails-api/descope/config/locales/en.yml +31 -0
- data/examples/ruby-on-rails-api/descope/config/puma.rb +35 -0
- data/examples/ruby-on-rails-api/descope/config/routes.rb +18 -0
- data/examples/ruby-on-rails-api/descope/config/storage.yml +34 -0
- data/examples/ruby-on-rails-api/descope/config.ru +6 -0
- data/examples/ruby-on-rails-api/descope/db/seeds.rb +9 -0
- data/examples/ruby-on-rails-api/descope/lib/assets/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/lib/tasks/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/log/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/package-lock.json +19680 -0
- data/examples/ruby-on-rails-api/descope/package.json +51 -0
- data/examples/ruby-on-rails-api/descope/public/404.html +67 -0
- data/examples/ruby-on-rails-api/descope/public/422.html +67 -0
- data/examples/ruby-on-rails-api/descope/public/500.html +66 -0
- data/examples/ruby-on-rails-api/descope/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/ruby-on-rails-api/descope/public/apple-touch-icon.png +0 -0
- data/examples/ruby-on-rails-api/descope/public/favicon.ico +0 -0
- data/examples/ruby-on-rails-api/descope/public/robots.txt +1 -0
- data/examples/ruby-on-rails-api/descope/storage/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/tmp/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/tmp/pids/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/tmp/storage/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/vendor/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/yarn.lock +10780 -0
- data/lib/descope/api/v1/auth/enchantedlink.rb +156 -0
- data/lib/descope/api/v1/auth/magiclink.rb +170 -0
- data/lib/descope/api/v1/auth/oauth.rb +72 -0
- data/lib/descope/api/v1/auth/otp.rb +186 -0
- data/lib/descope/api/v1/auth/password.rb +100 -0
- data/lib/descope/api/v1/auth/saml.rb +48 -0
- data/lib/descope/api/v1/auth/totp.rb +72 -0
- data/lib/descope/api/v1/auth.rb +452 -0
- data/lib/descope/api/v1/management/access_key.rb +81 -0
- data/lib/descope/api/v1/management/audit.rb +82 -0
- data/lib/descope/api/v1/management/authz.rb +165 -0
- data/lib/descope/api/v1/management/common.rb +147 -0
- data/lib/descope/api/v1/management/flow.rb +55 -0
- data/lib/descope/api/v1/management/password.rb +58 -0
- data/lib/descope/api/v1/management/permission.rb +48 -0
- data/lib/descope/api/v1/management/project.rb +53 -0
- data/lib/descope/api/v1/management/role.rb +48 -0
- data/lib/descope/api/v1/management/scim.rb +206 -0
- data/lib/descope/api/v1/management/sso_settings.rb +153 -0
- data/lib/descope/api/v1/management/tenant.rb +71 -0
- data/lib/descope/api/v1/management/user.rb +619 -0
- data/lib/descope/api/v1/management.rb +38 -0
- data/lib/descope/api/v1/session.rb +84 -0
- data/lib/descope/api/v1.rb +13 -0
- data/lib/descope/client.rb +6 -0
- data/lib/descope/exception.rb +50 -0
- data/lib/descope/mixins/common.rb +129 -0
- data/lib/descope/mixins/headers.rb +15 -0
- data/lib/descope/mixins/http.rb +133 -0
- data/lib/descope/mixins/initializer.rb +80 -0
- data/lib/descope/mixins/logging.rb +30 -0
- data/lib/descope/mixins/validation.rb +79 -0
- data/lib/descope/mixins.rb +22 -0
- data/lib/descope/version.rb +7 -0
- data/lib/descope.rb +9 -0
- data/lib/descope_client.rb +5 -0
- data/release-please-config.json +18 -0
- data/renovate.json +6 -0
- data/spec/factories/user.rb +16 -0
- data/spec/lib.descope/api/v1/auth/enchantedlink_spec.rb +159 -0
- data/spec/lib.descope/api/v1/auth/magiclink_spec.rb +282 -0
- data/spec/lib.descope/api/v1/auth/oauth_spec.rb +117 -0
- data/spec/lib.descope/api/v1/auth/otp_spec.rb +285 -0
- data/spec/lib.descope/api/v1/auth/password_spec.rb +124 -0
- data/spec/lib.descope/api/v1/auth/saml_spec.rb +55 -0
- data/spec/lib.descope/api/v1/auth/totp_spec.rb +70 -0
- data/spec/lib.descope/api/v1/auth_spec.rb +372 -0
- data/spec/lib.descope/api/v1/management/access_key_spec.rb +118 -0
- data/spec/lib.descope/api/v1/management/audit_spec.rb +78 -0
- data/spec/lib.descope/api/v1/management/authz_spec.rb +336 -0
- data/spec/lib.descope/api/v1/management/flow_spec.rb +78 -0
- data/spec/lib.descope/api/v1/management/password_spec.rb +25 -0
- data/spec/lib.descope/api/v1/management/permission_spec.rb +81 -0
- data/spec/lib.descope/api/v1/management/project_spec.rb +63 -0
- data/spec/lib.descope/api/v1/management/role_spec.rb +85 -0
- data/spec/lib.descope/api/v1/management/scim_spec.rb +312 -0
- data/spec/lib.descope/api/v1/management/sso_settings_spec.rb +172 -0
- data/spec/lib.descope/api/v1/management/tenant_spec.rb +141 -0
- data/spec/lib.descope/api/v1/management/user_spec.rb +667 -0
- data/spec/lib.descope/api/v1/session_spec.rb +117 -0
- data/spec/lib.descope/client_spec.rb +40 -0
- data/spec/spec_helper.rb +72 -0
- data/spec/support/client_config.rb +14 -0
- data/spec/support/dummy_class.rb +36 -0
- data/spec/support/utils.rb +32 -0
- metadata +420 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Descope
|
4
|
+
module Api
|
5
|
+
module V1
|
6
|
+
# Holds all session methods
|
7
|
+
module Session
|
8
|
+
include Descope::Mixins::Common
|
9
|
+
include Descope::Mixins::Common::EndpointsV1
|
10
|
+
include Descope::Mixins::Common::EndpointsV2
|
11
|
+
|
12
|
+
def token_validation_key(project_id)
|
13
|
+
get("#{PUBLIC_KEY_PATH}/#{project_id}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def refresh_session(refresh_token: nil, audience: nil)
|
17
|
+
# Validate a session token. Call this function for every incoming request to your
|
18
|
+
# private endpoints. Alternatively, use validate_and_refresh_session in order to
|
19
|
+
# automatically refresh expired sessions. If you need to use these specific claims
|
20
|
+
# [amr, drn, exp, iss, rexp, sub, jwt] in the top level of the response dict, please use
|
21
|
+
# them from the sessionToken key instead, as these claims will soon be deprecated from the top level
|
22
|
+
# of the response dict.
|
23
|
+
|
24
|
+
validate_refresh_token_not_nil(refresh_token)
|
25
|
+
validate_token(refresh_token, audience)
|
26
|
+
res = post(REFRESH_TOKEN_PATH, {}, {}, refresh_token)
|
27
|
+
generate_jwt_response(response_body: res, refresh_cookie: refresh_token, audience:)
|
28
|
+
end
|
29
|
+
|
30
|
+
def me(refresh_token = nil)
|
31
|
+
get(ME_PATH, {}, {}, refresh_token)
|
32
|
+
end
|
33
|
+
|
34
|
+
def sign_out(refresh_token = nil)
|
35
|
+
post(LOGOUT_PATH, {}, {}, refresh_token)
|
36
|
+
end
|
37
|
+
|
38
|
+
def sign_out_all(refresh_token = nil)
|
39
|
+
post(LOGOUT_ALL_PATH, {}, {}, refresh_token)
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_session(session_token: nil, audience: nil)
|
43
|
+
# Validate a session token. Call this function for every incoming request to your
|
44
|
+
# private endpoints. Alternatively, use validate_and_refresh_session in order to
|
45
|
+
# automatically refresh expired sessions. If you need to use these specific claims
|
46
|
+
# [amr, drn, exp, iss, rexp, sub, jwt] in the top level of the response dict, please use
|
47
|
+
# them from the sessionToken key instead, as these claims will soon be deprecated from the top level
|
48
|
+
# of the response dict.
|
49
|
+
# Return a hash includes the session token and all JWT claims
|
50
|
+
|
51
|
+
if session_token.nil? || session_token.empty?
|
52
|
+
raise Descope::AuthException.new('Session token is required for validation', code: 400)
|
53
|
+
end
|
54
|
+
|
55
|
+
@logger.debug("Validating session token: #{session_token}")
|
56
|
+
res = validate_token(session_token, audience)
|
57
|
+
@logger.debug("Session token validation response: #{res}")
|
58
|
+
# Duplicate for saving backward compatibility but keep the same structure as the refresh operation response
|
59
|
+
res[SESSION_TOKEN_NAME] = deep_copy(res)
|
60
|
+
session_props = adjust_properties(res, true)
|
61
|
+
@logger.debug("session validation jwt response properties: #{session_props}")
|
62
|
+
session_props
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_and_refresh_session(session_token: nil, refresh_token: nil, audience: nil)
|
66
|
+
# Validate the session token and refresh it if it has expired, the session token will automatically be refreshed.
|
67
|
+
# Either the session_token or the refresh_token must be provided.
|
68
|
+
# Call this function for every incoming request to your
|
69
|
+
# private endpoints. Alternatively, use validate_session to only validate the session.
|
70
|
+
|
71
|
+
raise Descope::AuthException.new('Session token is missing', code: 400) if session_token.nil?
|
72
|
+
|
73
|
+
begin
|
74
|
+
@logger.debug("Validating session token: #{session_token}")
|
75
|
+
validate_session(session_token:, audience:)
|
76
|
+
rescue Descope::AuthException
|
77
|
+
@logger.debug("Session is invalid, refreshing session with refresh token: #{refresh_token}")
|
78
|
+
refresh_session(refresh_token:, audience:)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'descope/api/v1/management'
|
2
|
+
require 'descope/api/v1/session'
|
3
|
+
require 'descope/api/v1/auth'
|
4
|
+
|
5
|
+
module Descope
|
6
|
+
module Api
|
7
|
+
module V1
|
8
|
+
include Descope::Api::V1::Management
|
9
|
+
include Descope::Api::V1::Session
|
10
|
+
include Descope::Api::V1::Auth
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Descope
|
2
|
+
# Default exception in namespace of Descope
|
3
|
+
# If you want to catch all exceptions, then you should use this one.
|
4
|
+
# Network exceptions are not included
|
5
|
+
class Exception < StandardError
|
6
|
+
attr_reader :error_data
|
7
|
+
|
8
|
+
def initialize(message, error_data = {})
|
9
|
+
super(message)
|
10
|
+
@error_data = error_data
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Parent for all exceptions that arise out of HTTP error responses.
|
15
|
+
class HTTPError < Descope::Exception
|
16
|
+
def headers
|
17
|
+
error_data[:headers]
|
18
|
+
end
|
19
|
+
|
20
|
+
def http_code
|
21
|
+
error_data[:code]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class AuthException < Descope::Exception; end
|
26
|
+
# exception for unauthorized requests, if you see it,
|
27
|
+
# probably Bearer Token is not set correctly
|
28
|
+
|
29
|
+
# exception for unset user_id, this might cause removal of
|
30
|
+
# all users, or other unexpected behaviour
|
31
|
+
class ArgumentException < Descope::Exception; end
|
32
|
+
|
33
|
+
# exception for invalid token when its empty
|
34
|
+
class InvalidToken < Descope::Exception; end
|
35
|
+
class InvalidParameter < Descope::Exception; end
|
36
|
+
class Unauthorized < Descope::HTTPError; end
|
37
|
+
# exception for not found resource, you query for an
|
38
|
+
# non-existent resource, or wrong path
|
39
|
+
class NotFound < Descope::HTTPError; end
|
40
|
+
class MethodNotAllowed < Descope::HTTPError; end
|
41
|
+
# exception for unknown error
|
42
|
+
class Unsupported < Descope::HTTPError; end
|
43
|
+
# exception for server error
|
44
|
+
class ServerError < Descope::HTTPError; end
|
45
|
+
# exception for incorrect request, you've sent wrong params
|
46
|
+
class BadRequest < Descope::HTTPError; end
|
47
|
+
class AccessDenied < Descope::HTTPError; end
|
48
|
+
class RateLimitException < Descope::HTTPError; end
|
49
|
+
class RequestTimeout < Descope::HTTPError; end
|
50
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../exception'
|
4
|
+
|
5
|
+
module Descope
|
6
|
+
module Mixins
|
7
|
+
# Common values and methods
|
8
|
+
module Common
|
9
|
+
DEFAULT_BASE_URL = 'https://api.descope.com' # pragma: no cover
|
10
|
+
DEFAULT_TIMEOUT_SECONDS = 60
|
11
|
+
DEFAULT_JWT_VALIDATION_LEEWAY = 5
|
12
|
+
PHONE_REGEX = %r{^(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[-./ \\]?(?:(?:\(?\d{1,}\)?[-./ \\]?){0,})(?:[-./ \\]?(?:#|ext\.?|extension|x)[-./ \\]?(\d+))?$}.freeze
|
13
|
+
|
14
|
+
SESSION_COOKIE_NAME = 'DS'
|
15
|
+
REFRESH_SESSION_COOKIE_NAME = 'DSR'
|
16
|
+
|
17
|
+
SESSION_TOKEN_NAME = 'sessionToken'
|
18
|
+
REFRESH_SESSION_TOKEN_NAME = 'refreshSessionToken'
|
19
|
+
COOKIE_DATA_NAME = 'cookieData'
|
20
|
+
|
21
|
+
REDIRECT_LOCATION_COOKIE_NAME = 'Location'
|
22
|
+
|
23
|
+
module DeliveryMethod
|
24
|
+
WHATSAPP = 1
|
25
|
+
SMS = 2
|
26
|
+
EMAIL = 3
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_method_string(method)
|
30
|
+
name = {
|
31
|
+
DeliveryMethod::WHATSAPP => 'whatsapp',
|
32
|
+
DeliveryMethod::SMS => 'sms',
|
33
|
+
DeliveryMethod::EMAIL => 'email'
|
34
|
+
}[method]
|
35
|
+
|
36
|
+
raise ArgumentException, "Unknown delivery method: #{method}" if name.nil?
|
37
|
+
|
38
|
+
name
|
39
|
+
end
|
40
|
+
|
41
|
+
def deep_copy(obj)
|
42
|
+
Marshal.load(Marshal.dump(obj))
|
43
|
+
end
|
44
|
+
|
45
|
+
module EndpointsV1
|
46
|
+
REFRESH_TOKEN_PATH = '/v1/auth/refresh'
|
47
|
+
SELECT_TENANT_PATH = '/v1/auth/tenant/select'
|
48
|
+
LOGOUT_PATH = '/v1/auth/logout'
|
49
|
+
LOGOUT_ALL_PATH = '/v1/auth/logoutall'
|
50
|
+
VALIDATE_SESSION_PATH = '/v1/auth/validate'
|
51
|
+
ME_PATH = '/v1/auth/me'
|
52
|
+
|
53
|
+
# accesskey
|
54
|
+
EXCHANGE_AUTH_ACCESS_KEY_PATH = '/v1/auth/accesskey/exchange'
|
55
|
+
|
56
|
+
# otp
|
57
|
+
SIGN_UP_AUTH_OTP_PATH = '/v1/auth/otp/signup'
|
58
|
+
SIGN_IN_AUTH_OTP_PATH = '/v1/auth/otp/signin'
|
59
|
+
SIGN_UP_OR_IN_AUTH_OTP_PATH = '/v1/auth/otp/signup-in'
|
60
|
+
VERIFY_CODE_AUTH_PATH = '/v1/auth/otp/verify'
|
61
|
+
UPDATE_USER_EMAIL_OTP_PATH = '/v1/auth/otp/update/email'
|
62
|
+
UPDATE_USER_PHONE_OTP_PATH = '/v1/auth/otp/update/phone'
|
63
|
+
|
64
|
+
# magiclink
|
65
|
+
SIGN_UP_AUTH_MAGICLINK_PATH = '/v1/auth/magiclink/signup'
|
66
|
+
SIGN_IN_AUTH_MAGICLINK_PATH = '/v1/auth/magiclink/signin'
|
67
|
+
SIGN_UP_OR_IN_AUTH_MAGICLINK_PATH = '/v1/auth/magiclink/signup-in'
|
68
|
+
VERIFY_MAGICLINK_AUTH_PATH = '/v1/auth/magiclink/verify'
|
69
|
+
GET_SESSION_MAGICLINK_AUTH_PATH = '/v1/auth/magiclink/pending-session'
|
70
|
+
UPDATE_USER_EMAIL_MAGICLINK_PATH = '/v1/auth/magiclink/update/email'
|
71
|
+
UPDATE_USER_PHONE_MAGICLINK_PATH = '/v1/auth/magiclink/update/phone'
|
72
|
+
|
73
|
+
# enchantedlink
|
74
|
+
SIGN_UP_AUTH_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/signup'
|
75
|
+
SIGN_IN_AUTH_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/signin'
|
76
|
+
SIGN_UP_OR_IN_AUTH_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/signup-in'
|
77
|
+
VERIFY_ENCHANTEDLINK_AUTH_PATH = '/v1/auth/enchantedlink/verify'
|
78
|
+
GET_SESSION_ENCHANTEDLINK_AUTH_PATH = '/v1/auth/enchantedlink/pending-session'
|
79
|
+
UPDATE_USER_EMAIL_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/update/email'
|
80
|
+
|
81
|
+
# oauth
|
82
|
+
OAUTH_START_PATH = '/v1/auth/oauth/authorize'
|
83
|
+
OAUTH_EXCHANGE_TOKEN_PATH = '/v1/auth/oauth/exchange'
|
84
|
+
OAUTH_CREATE_REDIRECT_URL_FOR_SIGN_IN_REQUEST_PATH = 'v1/auth/oauth/authorize/signin'
|
85
|
+
OAUTH_CREATE_REDIRECT_URL_FOR_SIGN_UP_REQUEST_PATH = 'v1/auth/oauth/authorize/signup'
|
86
|
+
|
87
|
+
# saml
|
88
|
+
AUTH_SAML_START_PATH = '/v1/auth/saml/authorize'
|
89
|
+
SAML_EXCHANGE_TOKEN_PATH = '/v1/auth/saml/exchange'
|
90
|
+
|
91
|
+
# totp
|
92
|
+
SIGN_UP_AUTH_TOTP_PATH = '/v1/auth/totp/signup'
|
93
|
+
VERIFY_TOTP_PATH = '/v1/auth/totp/verify'
|
94
|
+
UPDATE_TOTP_PATH = '/v1/auth/totp/update'
|
95
|
+
|
96
|
+
# webauthn
|
97
|
+
SIGN_UP_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/signup/start'
|
98
|
+
SIGN_UP_AUTH_WEBAUTHN_FINISH_PATH = '/v1/auth/webauthn/signup/finish'
|
99
|
+
SIGN_IN_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/signin/start'
|
100
|
+
SIGN_IN_AUTH_WEBAUTHN_FINISH_PATH = '/v1/auth/webauthn/signin/finish'
|
101
|
+
SIGN_UP_OR_IN_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/signup-in/start'
|
102
|
+
UPDATE_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/update/start'
|
103
|
+
UPDATE_AUTH_WEBAUTHN_FINISH_PATH = '/v1/auth/webauthn/update/finish'
|
104
|
+
|
105
|
+
# password
|
106
|
+
SIGN_UP_PASSWORD_PATH = '/v1/auth/password/signup'
|
107
|
+
SIGN_IN_PASSWORD_PATH = '/v1/auth/password/signin'
|
108
|
+
SEND_RESET_PASSWORD_PATH = '/v1/auth/password/reset'
|
109
|
+
UPDATE_PASSWORD_PATH = '/v1/auth/password/update'
|
110
|
+
REPLACE_PASSWORD_PATH = '/v1/auth/password/replace'
|
111
|
+
PASSWORD_POLICY_PATH = '/v1/auth/password/policy'
|
112
|
+
end
|
113
|
+
|
114
|
+
module EndpointsV2
|
115
|
+
PUBLIC_KEY_PATH = '/v2/keys'
|
116
|
+
end
|
117
|
+
|
118
|
+
module LoginOptions
|
119
|
+
attr_accessor :stepup, :mfa, :custom_claims
|
120
|
+
|
121
|
+
def initialize
|
122
|
+
@stepup = stepup || false
|
123
|
+
@mfa ||= false
|
124
|
+
@custom_claims ||= {}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Descope
|
2
|
+
module Mixins
|
3
|
+
module Headers
|
4
|
+
# Descope default headers
|
5
|
+
def client_headers
|
6
|
+
{
|
7
|
+
'Content-Type' => 'application/json',
|
8
|
+
'x-descope-sdk-name': 'ruby',
|
9
|
+
'x-descope-sdk-ruby-version': RUBY_VERSION,
|
10
|
+
'x-descope-sdk-version': Descope::SDK_VERSION,
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "addressable/uri"
|
3
|
+
require 'retryable'
|
4
|
+
require_relative '../exception'
|
5
|
+
|
6
|
+
module Descope
|
7
|
+
module Mixins
|
8
|
+
# HTTP-related methods
|
9
|
+
module HTTP
|
10
|
+
attr_accessor :headers, :base_uri, :timeout, :retry_count
|
11
|
+
|
12
|
+
DEFAULT_RETRIES = 3
|
13
|
+
MAX_ALLOWED_RETRIES = 10
|
14
|
+
MAX_REQUEST_RETRY_JITTER = 250
|
15
|
+
MAX_REQUEST_RETRY_DELAY = 1000
|
16
|
+
MIN_REQUEST_RETRY_DELAY = 250
|
17
|
+
BASE_DELAY = 100
|
18
|
+
|
19
|
+
%i[get post post_file post_form put patch delete delete_with_body].each do |method|
|
20
|
+
define_method(method) do |uri, body = {}, extra_headers = {}, pswd = nil|
|
21
|
+
body = body.delete_if { |_, v| v.nil? }
|
22
|
+
authorization_header(pswd) # This will set the pswd if provided, else default to the @default_pswd
|
23
|
+
|
24
|
+
@logger.debug "request => method: #{method}, uri: #{uri}, body: #{body}, extra_headers: #{extra_headers}}"
|
25
|
+
request_with_retry(method, uri, body, extra_headers)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def retry_options
|
30
|
+
sleep_timer = lambda do |attempt|
|
31
|
+
wait = BASE_DELAY * (2**attempt - 1) # Exponential delay with each subsequent request attempt.
|
32
|
+
wait += rand(wait + 1..wait + MAX_REQUEST_RETRY_JITTER) # Add jitter to the delay window.
|
33
|
+
wait = [MAX_REQUEST_RETRY_DELAY, wait].min # Cap delay at MAX_REQUEST_RETRY_DELAY.
|
34
|
+
wait = [MIN_REQUEST_RETRY_DELAY, wait].max # Ensure delay is no less than MIN_REQUEST_RETRY_DELAY.
|
35
|
+
wait / 1000.to_f.round(2) # convert ms to seconds
|
36
|
+
end
|
37
|
+
|
38
|
+
tries = 1 + [Integer(retry_count || DEFAULT_RETRIES), MAX_ALLOWED_RETRIES].min # Cap retries at MAX_ALLOWED_RETRIES
|
39
|
+
|
40
|
+
{
|
41
|
+
tries: tries,
|
42
|
+
sleep: sleep_timer,
|
43
|
+
on: Descope::RateLimitException
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def safe_parse_json(body)
|
48
|
+
@logger.debug "response => #{JSON.parse(body.to_s)}"
|
49
|
+
JSON.parse(body.to_s)
|
50
|
+
rescue JSON::ParserError
|
51
|
+
body
|
52
|
+
end
|
53
|
+
|
54
|
+
def encode_uri(uri)
|
55
|
+
encoded_uri = base_uri ? Addressable::URI.parse(uri).normalize : Addressable::URI.escape(uri)
|
56
|
+
@logger.debug "will call #{url(encoded_uri)}"
|
57
|
+
url(encoded_uri)
|
58
|
+
end
|
59
|
+
|
60
|
+
def url(path)
|
61
|
+
"#{@base_uri}#{path}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_headers(h = {})
|
65
|
+
raise ArgumentError, 'Headers must be an object which responds to #to_hash' unless h.respond_to?(:to_hash)
|
66
|
+
|
67
|
+
@headers ||= {}
|
68
|
+
@headers.merge!(h.to_hash)
|
69
|
+
end
|
70
|
+
|
71
|
+
def request_with_retry(method, uri, body = {}, extra_headers = {}, pswd = nil)
|
72
|
+
Retryable.retryable(retry_options) do
|
73
|
+
request(method, uri, body, extra_headers)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def request(method, uri, body = {}, extra_headers = {})
|
78
|
+
# @headers is getting the authorization header merged in initializer.rb
|
79
|
+
headers_debug = @headers.dup
|
80
|
+
if headers_debug['Authorization']
|
81
|
+
headers_debug['Authorization'] = headers_debug['Authorization'].gsub(/(.{10})\z/, '***********')
|
82
|
+
end
|
83
|
+
|
84
|
+
@logger.debug "base url: #{@base_uri}"
|
85
|
+
@logger.debug "request method: #{method}, uri: #{uri}, body: #{body}, extra_headers: #{extra_headers}, headers: #{headers_debug}"
|
86
|
+
result = case method
|
87
|
+
when :get
|
88
|
+
get_headers = @headers.merge({ params: body }).merge(extra_headers)
|
89
|
+
call(:get, encode_uri(uri), timeout, get_headers)
|
90
|
+
when :delete
|
91
|
+
delete_headers = @headers.merge({ params: body })
|
92
|
+
call(:delete, encode_uri(uri), timeout, delete_headers)
|
93
|
+
else
|
94
|
+
call(method, encode_uri(uri), timeout, @headers, body.to_json)
|
95
|
+
end
|
96
|
+
|
97
|
+
raise Descope::Unsupported.new("No response from server", code: 400) unless result && result.respond_to?(:code)
|
98
|
+
|
99
|
+
@logger.info "http status code: #{result.code}"
|
100
|
+
case result.code
|
101
|
+
when 200...226 then safe_parse_json(result.body)
|
102
|
+
when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers)
|
103
|
+
when 401 then raise Descope::Unauthorized.new(result.body, code: result.code, headers: result.headers)
|
104
|
+
when 403 then raise Descope::AccessDenied.new(result.body, code: result.code, headers: result.headers)
|
105
|
+
when 404 then raise Descope::NotFound.new(result.body, code: result.code, headers: result.headers)
|
106
|
+
when 405 then raise Descope::MethodNotAllowed.new(result.body, code: result.code, headers: result.headers)
|
107
|
+
when 429 then raise Descope::RateLimitException.new(result.body, code: result.code, headers: result.headers)
|
108
|
+
when 500 then raise Descope::ServerError.new(result.body, code: result.code, headers: result.headers)
|
109
|
+
else
|
110
|
+
raise Descope::Unsupported.new(result.body, code: result.code, headers: result.headers)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def call(method, url, timeout, headers, body = nil)
|
115
|
+
RestClient::Request.execute(
|
116
|
+
method:,
|
117
|
+
url:,
|
118
|
+
timeout:,
|
119
|
+
headers:,
|
120
|
+
payload: body
|
121
|
+
)
|
122
|
+
rescue RestClient::Exception => e
|
123
|
+
case e
|
124
|
+
when RestClient::RequestTimeout
|
125
|
+
raise Descope::RequestTimeout.new(e.message)
|
126
|
+
else
|
127
|
+
return e.response
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Descope
|
6
|
+
module Mixins
|
7
|
+
# Helper class for initializing the Descope API
|
8
|
+
module Initializer
|
9
|
+
attr_accessor :public_keys, :mlock
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
options = Hash[config.map { |(k, v)| [k.to_sym, v] }]
|
13
|
+
@base_uri = base_url(options)
|
14
|
+
@headers = client_headers
|
15
|
+
@project_id = options[:project_id] || ENV['DESCOPE_PROJECT_ID'] || ''
|
16
|
+
@public_key = options[:public_key] || ENV['DESCOPE_PUBLIC_KEY']
|
17
|
+
@mlock = Mutex.new
|
18
|
+
log_level = options[:log_level] || ENV['DESCOPE_LOG_LEVEL'] || 'info'
|
19
|
+
@logger ||= Descope::Mixins::Logging.logger_for(self.class.name, log_level)
|
20
|
+
|
21
|
+
@logger.debug("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@base_uri}")
|
22
|
+
|
23
|
+
if @public_key.nil?
|
24
|
+
@public_keys = {}
|
25
|
+
else
|
26
|
+
kid, pub_key, alg = validate_and_load_public_key(@public_key)
|
27
|
+
@public_keys = { kid => [pub_key, alg] }
|
28
|
+
end
|
29
|
+
|
30
|
+
@skip_verify = options[:skip_verify]
|
31
|
+
@secure = !@skip_verify
|
32
|
+
@management_key = options[:management_key] || ENV['DESCOPE_MANAGEMENT_KEY']
|
33
|
+
@logger.debug("Management Key ID: #{@management_key}")
|
34
|
+
@timeout_seconds = options[:timeout_seconds] || Common::DEFAULT_TIMEOUT_SECONDS
|
35
|
+
@jwt_validation_leeway = options[:jwt_validation_leeway] || Common::DEFAULT_JWT_VALIDATION_LEEWAY
|
36
|
+
|
37
|
+
if @project_id.to_s.empty?
|
38
|
+
raise AuthException.new(
|
39
|
+
'Unable to init Auth object because project_id cannot be empty. '\
|
40
|
+
'Set environment variable DESCOPE_PROJECT_ID or pass your Project ID to the init function.',
|
41
|
+
code: 400
|
42
|
+
)
|
43
|
+
else
|
44
|
+
initialize_api(options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.included(klass)
|
49
|
+
klass.send :prepend, Initializer
|
50
|
+
end
|
51
|
+
|
52
|
+
def base_url(options)
|
53
|
+
url = options[:descope_base_uri] || ENV['DESCOPE_BASE_URI'] || Common::DEFAULT_BASE_URL
|
54
|
+
return url if url.start_with? 'http'
|
55
|
+
|
56
|
+
raise AuthException.new('base url must start with http or https', code: 400)
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def authorization_header(pswd = nil)
|
61
|
+
pswd = @default_pswd if pswd.nil? || pswd.empty?
|
62
|
+
bearer = "#{@project_id}:#{pswd}"
|
63
|
+
add_headers('Authorization' => "Bearer #{bearer}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize_api(options)
|
67
|
+
initialize_v1(options)
|
68
|
+
@default_pswd = options.fetch(:management_key, ENV['DESCOPE_MANAGEMENT_KEY'])
|
69
|
+
authorization_header
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize_v1(_options)
|
73
|
+
extend Descope::Api::V1
|
74
|
+
extend Descope::Api::V1::Management
|
75
|
+
extend Descope::Api::V1::Auth
|
76
|
+
extend Descope::Api::V1::Session
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Descope
|
4
|
+
module Mixins
|
5
|
+
# Module to provide logger.
|
6
|
+
module Logging
|
7
|
+
|
8
|
+
def logger
|
9
|
+
# This is the magical bit that gets mixed into the other modules
|
10
|
+
@logger ||= Logging.logger_for(self.class.name, 'info')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Use a hash class-ivar to cache a unique Logger per class:
|
14
|
+
@loggers = {}
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def logger_for(classname, level)
|
18
|
+
@loggers[classname] ||= configure_logger_for(classname, level)
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure_logger_for(classname, level = 'info')
|
22
|
+
logger = Logger.new(STDOUT)
|
23
|
+
logger.level = Object.const_get("Logger::#{level.upcase}")
|
24
|
+
logger.progname = classname
|
25
|
+
logger
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Descope
|
4
|
+
module Mixins
|
5
|
+
# Module to provide validation for specific data structures.
|
6
|
+
module Validation
|
7
|
+
def validate_tenants(key_tenants)
|
8
|
+
raise ArgumentError, 'key_tenants should be an Array of hashes' unless key_tenants.is_a? Array
|
9
|
+
|
10
|
+
key_tenants.each do |tenant|
|
11
|
+
unless tenant.is_a? Hash
|
12
|
+
raise ArgumentError,
|
13
|
+
'Each tenant should be a Hash of tenant_id and optional role_names array'
|
14
|
+
end
|
15
|
+
|
16
|
+
tenant_symbolized = tenant.transform_keys(&:to_sym)
|
17
|
+
|
18
|
+
raise ArgumentError, "Missing tenant_id key in tenant: #{tenant}" unless tenant_symbolized.key?(:tenant_id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_login_id(login_id)
|
23
|
+
raise AuthException, 'login_id cannot be empty' unless login_id.is_a?(String) && !login_id.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate_user_id(user_id)
|
27
|
+
raise Descope::ArgumentException, 'Missing user id' if user_id.nil? || user_id.to_s.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_password(password)
|
31
|
+
raise AuthException, 'password cannot be empty' unless password.is_a?(String) && !password.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_email(email)
|
35
|
+
raise AuthException.new('email cannot be empty', code: 400) unless email.is_a?(String) && !email.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_token_not_empty(token)
|
39
|
+
raise AuthException.new('Token cannot be empty', code: 400) unless token.is_a?(String) && !token.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_refresh_token_not_nil(refresh_token)
|
43
|
+
return unless refresh_token.nil? || refresh_token.empty?
|
44
|
+
|
45
|
+
raise AuthException.new('Refresh token is required to refresh a session', code: 400)
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_phone(method, phone)
|
49
|
+
raise AuthException.new('Phone number cannot be empty', code: 400) unless phone.is_a?(String) && !phone.empty?
|
50
|
+
raise AuthException.new('Invalid phone number', code: 400) unless phone.match?(PHONE_REGEX)
|
51
|
+
raise AuthException.new('Invalid delivery method', code: 400) unless [
|
52
|
+
DeliveryMethod::WHATSAPP, DeliveryMethod::SMS
|
53
|
+
].include?(method)
|
54
|
+
end
|
55
|
+
|
56
|
+
def verify_provider(oauth_provider)
|
57
|
+
return false if oauth_provider.to_s.empty? || oauth_provider.nil?
|
58
|
+
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_tenant(tenant)
|
63
|
+
raise AuthException.new('Tenant cannot be empty', code: 400) unless tenant.is_a?(String) && !tenant.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_redirect_url(return_url)
|
67
|
+
raise AuthException.new('Return_url cannot be empty', code: 400) unless return_url.is_a?(String) && !return_url.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_code(code)
|
71
|
+
raise AuthException.new('Code cannot be empty', code: 400) unless code.is_a?(String) && !code.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_scim_group_id(group_id)
|
75
|
+
raise AuthException.new('SCIM Group ID cannot be empty', code: 400) unless group_id.is_a?(String) && !group_id.empty?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'uri'
|
3
|
+
require 'logger'
|
4
|
+
require 'jwt'
|
5
|
+
require 'descope/mixins/headers'
|
6
|
+
require 'descope/mixins/http'
|
7
|
+
require 'descope/mixins/initializer'
|
8
|
+
require 'descope/mixins/validation'
|
9
|
+
require 'descope/mixins/logging'
|
10
|
+
require 'descope/mixins/common'
|
11
|
+
require 'descope/api/v1'
|
12
|
+
|
13
|
+
module Descope
|
14
|
+
# Collecting dependencies here
|
15
|
+
module Mixins
|
16
|
+
include Descope::Mixins::Common
|
17
|
+
include Descope::Mixins::Headers
|
18
|
+
include Descope::Mixins::HTTP
|
19
|
+
include Descope::Mixins::Initializer
|
20
|
+
include Descope::Mixins::Logging
|
21
|
+
end
|
22
|
+
end
|
data/lib/descope.rb
ADDED