Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions app/controllers/authenticate_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,42 @@ class AuthenticateController < ApplicationController
include BasicAuthenticator
include AuthorizeResource

def oidc_authenticate_token
params.permit!

auth_token = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator],
data_object: Authentication::AuthnOidc::V2::DataObjects::TokenAuthenticator
).call(
parameters: params.to_hash.symbolize_keys,
request_ip: request.ip
) do |authenticator|
Authentication::AuthnOidc::V2::Strategies::Token.new(
authenticator: authenticator
).callback(request.authorization.to_s.split(' ').last || request.raw_post)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should verify that the first value of the split is "Bearer" and not just any word. To enforce the correct standard. WDYT?

end

render_authn_token(auth_token)
end

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!

auth_token = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator]
authenticator_type: params[:authenticator],
data_object: Authentication::AuthnOidc::V2::DataObjects::CodeRedirectAuthenticator
).call(
parameters: params.to_hash.symbolize_keys,
request_ip: request.ip
)
) do |authenticator|
Authentication::AuthnOidc::V2::Strategies::Code.new(
authenticator: authenticator
).callback(
params.to_hash.symbolize_keys
)
end

render_authn_token(auth_token)
Rails.logger.debug("AuthenticateController#authenticate_okta - authentication token: #{auth_token.inspect}")
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def index
render(
json: "#{namespace}::Views::ProviderContext".constantize.new.call(
authenticators: DB::Repository::AuthenticatorRepository.new(
data_object: "#{namespace}::DataObjects::Authenticator".constantize
data_object: "#{namespace}::DataObjects::CodeRedirectAuthenticator".constantize
).find_all(
account: params[:account],
type: params[:authenticator]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,43 @@ module Authentication
module AuthnOidc
module V2
module DataObjects
class Authenticator
class TokenAuthenticator
attr_reader :provider_uri, :client_id, :id_token_user_property, :account, :service_id

# required
# Nil methods to allow OIDC Client to function as intended
# attr_reader

def initialize(
provider_uri:,
id_token_user_property:,
client_id:,
account:,
service_id:,
name: nil,
**_
)
@account = account
@provider_uri = provider_uri
@id_token_user_property = id_token_user_property
@client_id = client_id
@service_id = service_id
@name = name
end

def claim_mapping
@id_token_user_property
end

def name
@name || @service_id.titleize
end

def resource_id
"#{account}:webservice:conjur/authn-oidc/#{service_id}"
end
end

class CodeRedirectAuthenticator
attr_reader :provider_uri, :client_id, :client_secret, :claim_mapping, :nonce, :state, :account
attr_reader :service_id, :redirect_uri, :response_type

Expand Down
15 changes: 10 additions & 5 deletions app/domain/authentication/authn_oidc/v2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def oidc_client
end
end

def callback(code:)
def validate_code(code:, nonce:, code_verifier:)
unless code.present?
raise Errors::Authentication::RequestBody::MissingRequestParam, 'code'
end
Expand All @@ -46,15 +46,20 @@ def callback(code:)
bearer_token = oidc_client.access_token!(
scope: true,
client_auth_method: :basic,
nonce: @authenticator.nonce
nonce: nonce,
code_verifier: code_verifier
)
id_token = bearer_token.id_token || bearer_token.access_token
@logger.debug("token: #{id_token.inspect}")
@logger.debug("id token: #{id_token.inspect}")

validate_token(token: id_token, nonce: nonce)
end

def validate_token(token:, nonce: nil)
begin
attempts ||= 0
decoded_id_token = @oidc_id_token.decode(
id_token,
token,
discovery_information.jwks
)
rescue Exception => e
Expand All @@ -71,7 +76,7 @@ def callback(code:)
decoded_id_token.verify!(
issuer: @authenticator.provider_uri,
client_id: @authenticator.client_id,
nonce: @authenticator.nonce
nonce: nonce
)
decoded_id_token
rescue OpenIDConnect::ValidationFailed => e
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Authentication
module AuthnOidc
module V2
class ResolveIdentity
class IdentityResolver
def call(identity:, account:, allowed_roles:)
# make sure role has a resource (ex. user, host)
roles = allowed_roles.select(&:resource?)
Expand Down
107 changes: 81 additions & 26 deletions app/domain/authentication/authn_oidc/v2/strategy.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,94 @@

module Authentication
module AuthnOidc
module V2
class Strategy
def initialize(
authenticator:,
client: Authentication::AuthnOidc::V2::Client,
logger: Rails.logger
)
@authenticator = authenticator
@client = client.new(authenticator: authenticator)
@logger = logger
module Strategies
class Utilities
def self.resolve_identity(jwt:, claim_mapping:, logger: Rails.logger)
logger.debug("claim mapping: #{claim_mapping}")
logger.debug("jwt: #{jwt.raw_attributes.inspect}")

identity = jwt.raw_attributes.with_indifferent_access[claim_mapping]
unless identity.present?
raise Errors::Authentication::AuthnOidc::IdTokenClaimNotFoundOrEmpty, claim_mapping
end

logger.debug("resolved identity: #{identity}")
identity
end
end

# Don't love this name...
def callback(args)
@logger.info("-- args: #{args.inspect}")
# TODO: Check that `code` and `state` attributes are present
raise Errors::Authentication::AuthnOidc::StateMismatch unless args[:state] == @authenticator.state
# Looks up an identity based on a provided JWT token.
class Token
def initialize(
authenticator:,
client: Authentication::AuthnOidc::V2::Client,
logger: Rails.logger,
utilities: Utilities
)
@authenticator = authenticator
@client = client.new(authenticator: authenticator)
@logger = logger
@utilities = utilities
end

# Don't love this name...
def callback(token)
# binding.pry
# raise if args are empty... it means we don't have a token
raise Errors::Authentication::AuthnOidc::MissingBearerToken unless token.present?

identity = resolve_identity(
jwt: @client.callback(
code: args[:code]
@utilities.resolve_identity(
jwt: @client.validate_token(token: token),
claim_mapping: @authenticator.claim_mapping
)
)
unless identity.present?
raise Errors::Authentication::AuthnOidc::IdTokenClaimNotFoundOrEmpty,
@authenticator.claim_mapping
end
identity
end

def resolve_identity(jwt:)
@logger.info(jwt.raw_attributes.inspect)
jwt.raw_attributes.with_indifferent_access[@authenticator.claim_mapping]
# Looks up an identity based on a provided OIDC Code.
class Code
REQUIRED_FIELDS = %i[code nonce code_verifier].freeze

def initialize(
authenticator:,
client: Authentication::AuthnOidc::V2::Client,
logger: Rails.logger,
utilities: Utilities
)
@authenticator = authenticator
@client = client.new(authenticator: authenticator)
@logger = logger
@utilities = utilities
end

def validate_required_fields(parameters)
REQUIRED_FIELDS.each do |field|
next if parameters.key?(field) && parameters[field].present?

raise(Errors::Authentication::RequestBody::MissingRequestParam, field)
end
end

# Don't love this name...
def callback(args)
@logger.info("-- args: #{args.inspect}")

# Check we have our required parameters
validate_required_fields(args)
# raise Errors::Authentication::RequestBody::MissingRequestParam, 'code' unless args[:code].present?
# raise Errors::Authentication::RequestBody::MissingRequestParam, 'state' unless args[:state].present?

# Ensure state matches the configured state
# raise Errors::Authentication::AuthnOidc::StateMismatch unless args[:state] == @authenticator.state

@utilities.resolve_identity(
jwt: @client.validate_code(
code: args[:code],
nonce: args[:nonce],
code_verifier: args[:code_verifier]
),
claim_mapping: @authenticator.claim_mapping
)
end
end
end
end
Expand Down
44 changes: 33 additions & 11 deletions app/domain/authentication/authn_oidc/v2/views/provider_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,51 @@ def initialize(

def call(authenticators:)
authenticators.map do |authenticator|
state_nonce_code_challenge_values = state_nonce_challenge
client = @client.new(authenticator: authenticator)
{
service_id: authenticator.service_id,
type: 'authn-oidc',
name: authenticator.name,
redirect_uri: generate_redirect_url(
client: @client.new(authenticator: authenticator),
authenticator: authenticator
authorization_endpoint: client.discovery_information.authorization_endpoint,
client_id: authenticator.client_id,
response_type: authenticator.response_type,
scope: authenticator.scope,
state: state_nonce_code_challenge_values[:state],
nonce: state_nonce_code_challenge_values[:nonce],
code_verifier: state_nonce_code_challenge_values[:code_verifier],
redirect_uri: authenticator.redirect_uri
# @client.new(authenticator: authenticator),
# client_id:
# authenticator: authenticator
)
}
}.merge(state_nonce_code_challenge_values)
end
end

def generate_redirect_url(client:, authenticator:)
def state_nonce_challenge
{
state: SecureRandom.hex(25),
nonce: SecureRandom.hex(30),
code_verifier: SecureRandom.hex(35)
}
end

def generate_redirect_url(authorization_endpoint:, client_id:, response_type:, scope:, state:, code_verifier:, nonce:, redirect_uri:)
# code_verifier = 'f387301683cb91e03f3f25af45ed180293a54541d314252665'
params = {
client_id: authenticator.client_id,
response_type: authenticator.response_type,
scope: ERB::Util.url_encode(authenticator.scope),
state: authenticator.state,
nonce: authenticator.nonce,
redirect_uri: ERB::Util.url_encode(authenticator.redirect_uri)
client_id: client_id,
response_type: response_type,
scope: ERB::Util.url_encode(scope),
state: state,
code_challenge: Digest::SHA256.base64digest(code_verifier).tr("+/", "-_").tr("=", ""),
code_challenge_method: 'S256',
nonce: nonce,
redirect_uri: ERB::Util.url_encode(redirect_uri)
}.map { |key, value| "#{key}=#{value}" }.join("&")

"#{client.discovery_information.authorization_endpoint}?#{params}"
"#{authorization_endpoint}?#{params}"
end
end
end
Expand Down
22 changes: 11 additions & 11 deletions app/domain/authentication/handler/authentication_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def initialize(
authn_repo: DB::Repository::AuthenticatorRepository,
namespace_selector: Authentication::Util::NamespaceSelector,
logger: Rails.logger,
authentication_error: LogMessages::Authentication::AuthenticationError
authentication_error: LogMessages::Authentication::AuthenticationError,
data_object: nil
)
@role = role
@resource = resource
Expand All @@ -22,17 +23,18 @@ def initialize(
namespace = namespace_selector.select(
authenticator_type: authenticator_type
)

@identity_resolver = "#{namespace}::ResolveIdentity".constantize
@strategy = "#{namespace}::Strategy".constantize
# Use the default authenticator data object if one was not provided
if data_object.nil?
data_object = "#{namespace}::DataObjects::Authenticator".constantize
end
@identity_resolver = "#{namespace}::IdentityResolver".constantize
@authn_repo = authn_repo.new(
data_object: "#{namespace}::DataObjects::Authenticator".constantize
data_object: data_object
)
end

def call(parameters:, request_ip:)
raise Errors::Authentication::RequestBody::MissingRequestParam, parameters[:code] unless parameters[:code]
raise Errors::Authentication::RequestBody::MissingRequestParam, parameters[:state] unless parameters[:state]
def call(parameters:, request_ip:, &block)
@logger.info("parameters: #{parameters.inspect}")
# Load Authenticator policy and values (validates data stored as variables)
authenticator = @authn_repo.find(
type: @authenticator_type,
Expand All @@ -48,9 +50,7 @@ def call(parameters:, request_ip:)
end

role = @identity_resolver.new.call(
identity: @strategy.new(
authenticator: authenticator
).callback(parameters),
identity: block.call(authenticator),
account: parameters[:account],
allowed_roles: @role.that_can(
:authenticate,
Expand Down
4 changes: 4 additions & 0 deletions app/domain/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ module AuthnOidc
code: "CONJ00130E"
)

MissingBearerToken = ::Util::TrackableErrorClass.new(
msg: "Bearer Token must be provided either as an authorization header or in the request body",
code: "CONJ00133E"
)
end

module AuthnIam
Expand Down
Loading