Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authn jwt refactor v4 #2817

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ gem 'rack-rewrite'

gem 'dry-struct'
gem 'dry-types'
gem 'dry-validation'
gem 'net-ldap'

# for AWS rotator
Expand Down
15 changes: 15 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,17 @@ GEM
dry-core (0.7.1)
concurrent-ruby (~> 1.0)
dry-inflector (0.2.1)
dry-initializer (3.1.1)
dry-logic (1.2.0)
concurrent-ruby (~> 1.0)
dry-core (~> 0.5, >= 0.5)
dry-schema (1.10.6)
concurrent-ruby (~> 1.0)
dry-configurable (~> 0.13, >= 0.13.0)
dry-core (~> 0.5, >= 0.5)
dry-initializer (~> 3.0)
dry-logic (~> 1.2)
dry-types (~> 1.5)
dry-struct (1.4.0)
dry-core (~> 0.5, >= 0.5)
dry-types (~> 1.5)
Expand All @@ -207,6 +215,12 @@ GEM
dry-core (~> 0.5, >= 0.5)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
dry-validation (1.8.1)
concurrent-ruby (~> 1.0)
dry-container (~> 0.7, >= 0.7.1)
dry-core (~> 0.5, >= 0.5)
dry-initializer (~> 3.0)
dry-schema (~> 1.8, >= 1.8.0)
erubi (1.12.0)
event_emitter (0.2.6)
eventmachine (1.2.7)
Expand Down Expand Up @@ -519,6 +533,7 @@ DEPENDENCIES
debase (~> 0.2.5.beta2)
dry-struct
dry-types
dry-validation
event_emitter
faye-websocket
ffi (>= 1.9.24)
Expand Down
72 changes: 35 additions & 37 deletions app/controllers/authenticate_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,31 @@ class AuthenticateController < ApplicationController
include BasicAuthenticator
include AuthorizeResource

def oidc_authenticate_code_redirect
# TODO: need a mechanism for an authenticator strategy to define the required
# params. This will likely need to be done via the Handler.
params.permit!
def authenticate_via_get
handler = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator]
)

# Allow an authenticator to define the params it's expecting
allowed_params = params.permit(handler.params_allowed)

auth_token = handler.call(
parameters: allowed_params.to_h.symbolize_keys,
request_ip: request.ip
)

render_authn_token(auth_token)
rescue => e
log_backtrace(e)
raise e
end

def authenticate_via_post
auth_token = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator]
).call(
parameters: params.to_hash.symbolize_keys,
parameters: params,
request_body: request.body.read,
request_ip: request.ip
)

Expand All @@ -22,6 +38,20 @@ def oidc_authenticate_code_redirect
raise e
end

def authenticator_status
Authentication::Handler::StatusHandler.new(
authenticator_type: params[:authenticator]
).call(
parameters: params.permit(:account, :service_id, :authenticator).to_hash.symbolize_keys,
role: current_user,
request_ip: request.ip
)
render(json: { status: "ok" })
rescue => e
log_backtrace(e)
render(status_failure_response(e))
end

def index
authenticators = {
# Installed authenticator plugins
Expand Down Expand Up @@ -68,18 +98,6 @@ def status_input
)
end

def authn_jwt_status
params[:authenticator] = "authn-jwt"
Authentication::AuthnJwt::ValidateStatus.new.call(
authenticator_status_input: status_input,
enabled_authenticators: Authentication::InstalledAuthenticators.enabled_authenticators_str
)
render(json: { status: "ok" })
rescue => e
log_backtrace(e)
render(status_failure_response(e))
end

def update_config
Authentication::UpdateAuthenticatorConfig.new.(
update_config_input: update_config_input
Expand Down Expand Up @@ -118,23 +136,6 @@ def login
handle_login_error(e)
end

def authenticate_jwt
params[:authenticator] = "authn-jwt"
authn_token = Authentication::AuthnJwt::OrchestrateAuthentication.new.call(
authenticator_input: authenticator_input_without_credentials,
enabled_authenticators: Authentication::InstalledAuthenticators.enabled_authenticators_str
)
render_authn_token(authn_token)
rescue => e
# At this point authenticator_input.username is always empty (e.g. cucumber:user:USERNAME_MISSING)
log_audit_failure(
authn_params: authenticator_input,
audit_event_class: Audit::Event::Authn::Authenticate,
error: e
)
handle_authentication_error(e)
end

# Update the input to have the username from the token and authenticate
def authenticate_oidc
params[:authenticator] = "authn-oidc"
Expand Down Expand Up @@ -302,9 +303,6 @@ def handle_authentication_error(err)
when Errors::Authentication::RequestBody::MissingRequestParam
raise BadRequest

when Errors::Conjur::RequestedResourceNotFound
raise RecordNotFound.new(err.message)

when Errors::Authentication::Jwt::TokenExpired
raise Unauthorized.new(err.message, true)

Expand Down
86 changes: 56 additions & 30 deletions app/db/repository/authenticator_repository.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
module DB
module Repository
# This class is responsible for loading the variables associated with a
# particular type of authenticator. Each authenticator requires a Data
# Object and Data Object Contract (for validation). Data Objects that
# fail validation are not returned.
#
# This class includes two public methods:
# - `find_all` - returns all available authenticators of a specified type
# from an account
# - `find` - returns a single authenticator based on the provided type,
# account, and service identifier.
#
class AuthenticatorRepository
def initialize(
data_object:,
Expand All @@ -8,24 +19,20 @@ def initialize(
)
@resource_repository = resource_repository
@data_object = data_object
@contract = "#{data_object}Contract".constantize.new(utils: ::Util::ContractUtils)
@logger = logger
end

def find_all(type:, account:)
@resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:webservice:conjur/#{type}/%"
)
).all.map do |webservice|
authenticator_webservices(type: type, account: account).map do |webservice|
service_id = service_id_from_resource_id(webservice.id)

# Querying for the authenticator webservice above includes the webservices
# for the authenticator status. The filter below removes webservices that
# don't match the authenticator policy.
next unless webservice.id.split(':').last == "conjur/#{type}/#{service_id}"

load_authenticator(account: account, service_id: service_id, type: type)
begin
load_authenticator(account: account, service_id: service_id, type: type)
rescue => e
@logger.info("failed to load #{type} authenticator '#{service_id}' do to validation failure: #{e.message}")
nil
end
end.compact
end

Expand All @@ -36,17 +43,29 @@ def find(type:, account:, service_id:)
"#{account}:webservice:conjur/#{type}/#{service_id}"
)
).first
return unless webservice
unless webservice
raise Errors::Authentication::Security::WebserviceNotFound, "#{type}/#{service_id}"
end

load_authenticator(account: account, service_id: service_id, type: type)
end

def exists?(type:, account:, service_id:)
@resource_repository.with_pk("#{account}:webservice:conjur/#{type}/#{service_id}") != nil
end

private

def authenticator_webservices(type:, account:)
@resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:webservice:conjur/#{type}/%"
)
).all.select do |webservice|
# Querying for the authenticator webservice above includes the webservices
# for the authenticator status. The filter below removes webservices that
# don't match the authenticator policy.
webservice.id.split(':').last.match?(%r{^conjur/#{type}/[\w\-_]+$})
end
end

def service_id_from_resource_id(id)
full_id = id.split(':').last
full_id.split('/')[2]
Expand All @@ -59,26 +78,33 @@ def load_authenticator(type:, account:, service_id:)
"#{account}:variable:conjur/#{type}/#{service_id}/%"
)
).eager(:secrets).all

args_list = {}.tap do |args|
args[:account] = account
args[:service_id] = service_id
variables.each do |variable|
next unless variable.secret

args[variable.resource_id.split('/')[-1].underscore.to_sym] = variable.secret.value
# If variable exists but does not have a secret, set the value to an empty string.
# This is used downstream for validating if a variable has been set or not, and thus,
# what error to raise.
value = variable.secret ? variable.secret.value : ''
args[variable.resource_id.split('/')[-1].underscore.to_sym] = value
end
end

begin
allowed_args = %i[account service_id] +
@data_object.const_get(:REQUIRED_VARIABLES) +
@data_object.const_get(:OPTIONAL_VARIABLES)
args_list = args_list.select { |key, value| allowed_args.include?(key) && value.present? }
@data_object.new(**args_list)
rescue ArgumentError => e
@logger.debug("DB::Repository::AuthenticatorRepository.load_authenticator - exception: #{e}")
nil
# Validate the variables against the authenticator contract
result = @contract.call(args_list)
if result.success?
@data_object.new(**result.to_h)
else
errors = result.errors
@logger.info(errors.to_h.inspect)

# If contract fails, raise the first defined exception...
error = errors.first
raise(error.meta[:exception]) if error.meta[:exception].present?

# Otherwise, it's a validation error so raise the appropriate exception
raise(Errors::Conjur::RequiredSecretMissing,
"#{account}:variable:conjur/#{type}/#{service_id}/#{error.path.first.to_s.dasherize}")
end
end
end
Expand Down