Skip to content

Commit

Permalink
Revert "Remove support for disabling the non-PKCE OIDC authentication…
Browse files Browse the repository at this point in the history
… flow"

This reverts commit 753e359.
  • Loading branch information
jvanderhoof committed Feb 1, 2023
1 parent 753e359 commit 6e2ed7e
Show file tree
Hide file tree
Showing 25 changed files with 1,300 additions and 467 deletions.
17 changes: 9 additions & 8 deletions app/db/repository/authenticator_repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ class AuthenticatorRepository
def initialize(
data_object:,
resource_repository: ::Resource,
logger: Rails.logger
# ,
# pkce_support_enabled: Rails.configuration.feature_flags.enabled?(:pkce_support)
logger: Rails.logger,
pkce_support_enabled: Rails.configuration.feature_flags.enabled?(:pkce_support)
)
@resource_repository = resource_repository
@data_object = data_object
@logger = logger
# @pkce_support_enabled = pkce_support_enabled
@pkce_support_enabled = pkce_support_enabled
end

def find_all(type:, account:)
Expand Down Expand Up @@ -74,10 +73,12 @@ def load_authenticator(type:, account:, service_id:)
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? }
if @pkce_support_enabled
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? }
end
@data_object.new(**args_list)
rescue ArgumentError => e
@logger.debug("DB::Repository::AuthenticatorRepository.load_authenticator - exception: #{e}")
Expand Down
7 changes: 6 additions & 1 deletion app/domain/authentication/authn_oidc/authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ def status(authenticator_status_input:)
# is done, the following check can be removed.

# Attempt to load the V2 version of the OIDC Authenticator
data_object = if Rails.configuration.feature_flags.enabled?(:pkce_support)
Authentication::AuthnOidc::PkceSupportFeature::DataObjects::Authenticator
else
Authentication::AuthnOidc::V2::DataObjects::Authenticator
end
authenticator = DB::Repository::AuthenticatorRepository.new(
data_object: Authentication::AuthnOidc::V2::DataObjects::Authenticator
data_object: data_object
).find(
type: authenticator_status_input.authenticator_name,
account: authenticator_status_input.account,
Expand Down
115 changes: 115 additions & 0 deletions app/domain/authentication/authn_oidc/pkce_support_feature/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
module Authentication
module AuthnOidc
module PkceSupportFeature
class Client
def initialize(
authenticator:,
client: ::OpenIDConnect::Client,
oidc_id_token: ::OpenIDConnect::ResponseObject::IdToken,
discovery_configuration: ::OpenIDConnect::Discovery::Provider::Config,
cache: Rails.cache,
logger: Rails.logger
)
@authenticator = authenticator
@client = client
@oidc_id_token = oidc_id_token
@discovery_configuration = discovery_configuration
@cache = cache
@logger = logger
end

def oidc_client
@oidc_client ||= begin
issuer_uri = URI(@authenticator.provider_uri)
@client.new(
identifier: @authenticator.client_id,
secret: @authenticator.client_secret,
redirect_uri: @authenticator.redirect_uri,
scheme: issuer_uri.scheme,
host: issuer_uri.host,
port: issuer_uri.port,
authorization_endpoint: URI(discovery_information.authorization_endpoint).path,
token_endpoint: URI(discovery_information.token_endpoint).path,
userinfo_endpoint: URI(discovery_information.userinfo_endpoint).path,
jwks_uri: URI(discovery_information.jwks_uri).path,
end_session_endpoint: URI(discovery_information.end_session_endpoint).path
)
end
end

def callback(code:, nonce:, code_verifier: nil)
oidc_client.authorization_code = code
access_token_args = { scope: true, client_auth_method: :basic }
access_token_args[:code_verifier] = code_verifier if code_verifier.present?
begin
bearer_token = oidc_client.access_token!(**access_token_args)
rescue Rack::OAuth2::Client::Error => e
# Only handle the expected errors related to access token retrieval.
case e.message
when /PKCE verification failed/
raise Errors::Authentication::AuthnOidc::TokenRetrievalFailed,
'PKCE verification failed'
when /The authorization code is invalid or has expired/
raise Errors::Authentication::AuthnOidc::TokenRetrievalFailed,
'Authorization code is invalid or has expired'
when /Code not valid/
raise Errors::Authentication::AuthnOidc::TokenRetrievalFailed,
'Authorization code is invalid'
end
raise e
end
id_token = bearer_token.id_token || bearer_token.access_token

begin
attempts ||= 0
decoded_id_token = @oidc_id_token.decode(
id_token,
discovery_information.jwks
)
rescue StandardError => e
attempts += 1
raise e if attempts > 1

# If the JWKS verification fails, blow away the existing cache and
# try again. This is intended to handle the case where the OIDC certificate
# changes, and we want to cache the new certificate without decode failing.
discovery_information(invalidate: true)
retry
end

begin
decoded_id_token.verify!(
issuer: @authenticator.provider_uri,
client_id: @authenticator.client_id,
nonce: nonce
)
rescue OpenIDConnect::ResponseObject::IdToken::InvalidNonce
raise Errors::Authentication::AuthnOidc::TokenVerificationFailed,
'Provided nonce does not match the nonce in the JWT'
rescue OpenIDConnect::ResponseObject::IdToken::ExpiredToken
raise Errors::Authentication::AuthnOidc::TokenVerificationFailed,
'JWT has expired'
rescue OpenIDConnect::ValidationFailed => e
raise Errors::Authentication::AuthnOidc::TokenVerificationFailed,
e.message
end
decoded_id_token
end

def discovery_information(invalidate: false)
@cache.fetch(
"#{@authenticator.account}/#{@authenticator.service_id}/#{URI::Parser.new.escape(@authenticator.provider_uri)}",
force: invalidate,
skip_nil: true
) do
@discovery_configuration.discover!(@authenticator.provider_uri)
rescue HTTPClient::ConnectTimeoutError, Errno::ETIMEDOUT => e
raise Errors::Authentication::OAuth::ProviderDiscoveryTimeout.new(@authenticator.provider_uri, e.message)
rescue => e
raise Errors::Authentication::OAuth::ProviderDiscoveryFailed.new(@authenticator.provider_uri, e.message)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Authentication
module AuthnOidc
module PkceSupportFeature
module DataObjects
class Authenticator

REQUIRED_VARIABLES = %i[provider_uri client_id client_secret claim_mapping].freeze
OPTIONAL_VARIABLES = %i[redirect_uri response_type provider_scope name token_ttl].freeze

attr_reader(
:provider_uri,
:client_id,
:client_secret,
:claim_mapping,
:account,
:service_id,
:redirect_uri,
:response_type
)

def initialize(
provider_uri:,
client_id:,
client_secret:,
claim_mapping:,
account:,
service_id:,
redirect_uri: nil,
name: nil,
response_type: 'code',
provider_scope: nil,
token_ttl: 'PT8M'
)
@account = account
@provider_uri = provider_uri
@client_id = client_id
@client_secret = client_secret
@claim_mapping = claim_mapping
@response_type = response_type
@service_id = service_id
@name = name
@provider_scope = provider_scope
@redirect_uri = redirect_uri
@token_ttl = token_ttl
end

def scope
(%w[openid email profile] + [*@provider_scope.to_s.split(' ')]).uniq.join(' ')
end

def name
@name || @service_id.titleize
end

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

# Returns the validity duration, in seconds, of an instance's access tokens.
def token_ttl
ActiveSupport::Duration.parse(@token_ttl)
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
raise Errors::Authentication::DataObjects::InvalidTokenTTL.new(resource_id, @token_ttl)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Authentication
module AuthnOidc
module PkceSupportFeature
class ResolveIdentity
def call(identity:, account:, allowed_roles:)
# make sure role has a resource (ex. user, host)
roles = allowed_roles.select(&:resource?)

roles.each do |role|
role_account, _, role_id = role.id.split(':')
return role if role_account == account && identity == role_id
end

raise(Errors::Authentication::Security::RoleNotFound, identity)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Authentication
module AuthnOidc
module PkceSupportFeature
class Strategy
def initialize(
authenticator:,
client: Authentication::AuthnOidc::PkceSupportFeature::Client,
logger: Rails.logger
)
@authenticator = authenticator
@client = client.new(authenticator: authenticator)
@logger = logger
end

def callback(args)
%i[code nonce].each do |param|
unless args[param].present?
raise Errors::Authentication::RequestBody::MissingRequestParam, param.to_s
end
end

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

def resolve_identity(jwt:)
jwt.raw_attributes.with_indifferent_access[@authenticator.claim_mapping]
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'securerandom'
require 'digest'

module Authentication
module AuthnOidc
module PkceSupportFeature
module Views
class ProviderContext
def initialize(
client: Authentication::AuthnOidc::PkceSupportFeature::Client,
digest: Digest::SHA256,
random: SecureRandom,
logger: Rails.logger
)
@client = client
@logger = logger
@digest = digest
@random = random
end

def call(authenticators:)
authenticators.map do |authenticator|
begin
nonce = @random.hex(25)
code_verifier = @random.hex(25)
code_challenge = @digest.base64digest(code_verifier).tr("+/", "-_").tr("=", "")
{
service_id: authenticator.service_id,
type: 'authn-oidc',
name: authenticator.name,
nonce: nonce,
code_verifier: code_verifier,
redirect_uri: generate_redirect_url(
client: @client.new(authenticator: authenticator),
authenticator: authenticator,
nonce: nonce,
code_challenge: code_challenge
)
}
rescue Errors::Authentication::OAuth::ProviderDiscoveryFailed,
Errors::Authentication::OAuth::ProviderDiscoveryTimeout
@logger.warn("Authn-OIDC '#{authenticator.service_id}' provider-uri: '#{authenticator.provider_uri}' is unreachable")
nil
end
end.compact
end

def generate_redirect_url(client:, authenticator:, nonce:, code_challenge:)
params = {
client_id: authenticator.client_id,
response_type: authenticator.response_type,
scope: ERB::Util.url_encode(authenticator.scope),
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: 'S256',
redirect_uri: ERB::Util.url_encode(authenticator.redirect_uri)
}
formatted_params = params.map { |key, value| "#{key}=#{value}" }.join("&")

"#{client.discovery_information.authorization_endpoint}?#{formatted_params}"
end
end
end
end
end
end
Loading

0 comments on commit 6e2ed7e

Please sign in to comment.