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

Adds PKCE Support to Conjur #2678

Merged
merged 2 commits into from
Dec 9, 2022
Merged

Adds PKCE Support to Conjur #2678

merged 2 commits into from
Dec 9, 2022

Conversation

jvanderhoof
Copy link
Contributor

@jvanderhoof jvanderhoof commented Nov 23, 2022

Desired Outcome

This PR attempts to accomplish two objectives:

  1. Segregate the PKCE Support using the recently added Feature Flag functionality.
  2. Provide a pattern for abstracting and testing workflows managed by a feature flag.

Implemented Changes

PKCE Support

  • Primary Changes:
    • Adds a generated code challenge and code verify to provider API
    • OIDC authentication endpoint now requires code verify, which is used in the provider token request
  • Secondary Changes:
    • A unique nonce is generated on each Provider API request
    • State is no longer generated on Provider API request. A client should manage its own state value without Conjur being aware.
    • Adds required and optional fields to the Authenticator data object. This allows the repository to load only relevant variables.
    • Moves the required parameters check up to the Strategy. This allows a handler to be unconcerned with authentication details.

Feature Flag Patterning

  • Primary Changes:
    • Adds the ClimateControl gem and a small helper method (with_feature_enabled) to run RSpec tests with a feature toggled on (flags are off by default). This prevents us using environment variables on the container or test level.

      Note: we still need to develop an approach for testing Cucumber tests as they operate against an external container.

Reviewers should step through commits.

Connected Issue/Story

N/A

Definition of Done

Changelog

  • The CHANGELOG has been updated, or
  • This PR does not include user-facing changes and doesn't require a
    CHANGELOG update

Test coverage

  • This PR includes new unit and integration tests to go with the code
    changes, or
  • The changes in this PR do not require tests

Documentation

  • Docs (e.g. READMEs) were updated in this PR
  • A follow-up issue to update official docs has been filed here: ONYX-27159
  • This PR does not require updating any documentation

Behavior

  • This PR changes product behavior and has been reviewed by a PO, or
  • These changes are part of a larger initiative that will be reviewed later, or
  • No behavior was changed with this PR

Security

  • Security architect has reviewed the changes in this PR,
  • These changes are part of a larger initiative with a separate security review, or
  • There are no security aspects to these changes

end
end

def callback(code:, nonce:, code_verifier:)
Copy link

Choose a reason for hiding this comment

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

Method callback has 51 lines of code (exceeds 25 allowed). Consider refactoring.

module AuthnOidc
module PkceSupportFeature
module DataObjects
class Authenticator
Copy link

Choose a reason for hiding this comment

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

Authentication::AuthnOidc::PkceSupportFeature::DataObjects::Authenticator has no descriptive comment

@@ -0,0 +1,19 @@
module Authentication
module AuthnOidc
Copy link

Choose a reason for hiding this comment

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

Similar blocks of code found in 2 locations. Consider refactoring.

@logger = logger
end

def oidc_client
Copy link

Choose a reason for hiding this comment

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

Complex method Authentication::AuthnOidc::PkceSupportFeature::Client#oidc_client (43.4)

end
end

def callback(code:, nonce:, code_verifier:)
Copy link

Choose a reason for hiding this comment

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

Complex method Authentication::AuthnOidc::PkceSupportFeature::Client#callback (32.5)

@jvanderhoof jvanderhoof force-pushed the pkce-support branch 2 times, most recently from c188f13 to adece22 Compare November 23, 2022 19:06
end
end

def callback(code:, nonce:, code_verifier:)
Copy link

Choose a reason for hiding this comment

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

Method callback has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.

code: "CONJ00133E"
)

# TODO - this is related to the feature flag 'pkce_support'. Once
Copy link

Choose a reason for hiding this comment

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

Annotation keywords like TODO should be all upper case, followed by a colon, and a space, then a note describing the problem.

OPTIONAL_VARIABLES = %i[redirect_uri response_type provider_scope name].freeze

attr_reader :provider_uri, :client_id, :client_secret, :claim_mapping, :account
attr_reader :service_id, :redirect_uri, :response_type
Copy link

Choose a reason for hiding this comment

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

Group together all attr_reader attributes.

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

attr_reader :provider_uri, :client_id, :client_secret, :claim_mapping, :account
Copy link

Choose a reason for hiding this comment

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

Group together all attr_reader attributes.

decoded_id_token
end

def discovery_information(invalidate: false)
Copy link

Choose a reason for hiding this comment

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

Complex method Authentication::AuthnOidc::PkceSupportFeature::Client#discovery_information (21.8)

@jvanderhoof jvanderhoof marked this pull request as ready for review November 23, 2022 20:32
@jvanderhoof jvanderhoof requested a review from a team as a code owner November 23, 2022 20:32
CHANGELOG.md Outdated
### Added
- Provides support for PKCE in the OIDC Authenticator code redirect workflow.
This is disabled by default, but is available under the
`CONJUR_FEATURE_POLICY_LOAD_EXTENSIONS` feature flag.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be CONJUR_FEATURE_PKCE_SUPPORT_ENABLED, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

doh! good catch.

Copy link
Contributor

@micahlee micahlee left a comment

Choose a reason for hiding this comment

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

Hey @jvanderhoof,

I left some thoughts for you regarding the testing approach for feature flags. This isn't something I feel super strongly about, so you can tell me if you rather keep it as it is.

Thanks!

(Also, I'm assuming all of the logic itself is previously reviewed, and we're only reviewing the feature flag part, is that correct?)

@@ -150,3 +150,11 @@ def conjur_server_dir
# Navigate from its directory (/bin) to the root Conjur server directory
Pathname.new(File.join(File.dirname(conjurctl_path), '..')).cleanpath
end

# Toggle a feature flag on for a particular set of tests.
def with_feature_enabled(feature, &block)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not opposed to this, but I am concerned it:

A) Adds more "magic"/black box logic our tests, when we've been trending to more "obvious face value" specs.

B) Encourages the use of integration-style specs that depend on the actual environment, rather than injecting dependencies.

I'll add some more specific comments in the use of this in the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handling the flag toggle with dependency injection means this can be removed. It's MUCH cleaner and prevents us from needing to mutate the environment variable during testing. I'm removing this and the gem.

let(:arguments) { %i[provider_uri client_id client_secret claim_mapping nonce state] }
%w[true false].each do |enabled|
context "with flag #{enabled}" do
with_feature_enabled(:pkce_support) do
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't look like enabled is actually used here to turn it on or off.

Comment on lines 10 to 14
if Rails.configuration.feature_flags.enabled?(:pkce_support)
Authentication::AuthnOidc::PkceSupportFeature::DataObjects::Authenticator
else
Authentication::AuthnOidc::V2::DataObjects::Authenticator
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Here's an example of where I'd rather see this driven by test inputs, the enabled option above, rather than indirectly through the feature flag environment variable.

begin
@data_object.new(**args_list)
if Rails.configuration.feature_flags.enabled?(:pkce_support)
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, with testing in mind. I'd rather see either configuration, feature_flags, or even pkce_support passed in as a dependency to the class, and use that when unit testing this, rather than setting the environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Brillant. I didn't even think about that as an option 😢. That may also allow us to remove the toggle in testing...

@jvanderhoof
Copy link
Contributor Author

Hey @jvanderhoof,

I left some thoughts for you regarding the testing approach for feature flags. This isn't something I feel super strongly about, so you can tell me if you rather keep it as it is.

Thanks!

(Also, I'm assuming all of the logic itself is previously reviewed, and we're only reviewing the feature flag part, is that correct?)

The flagged functionality was reviewed, but if you have any concerns, I'd love the feedback. The original code was merged in more of a rush than I felt was ideal.

@micahlee
Copy link
Contributor

micahlee commented Dec 6, 2022

Hey @jvanderhoof, I think you meant to rebase this PR, rather than merge master in:
image

Gemfile Outdated
@@ -80,7 +80,6 @@ gem 'i18n', '~> 1.8.11'
group :development, :test do
gem 'aruba'
gem 'ci_reporter_rspec'
gem 'climate_control'
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we squash these changes down so that the gemfile changes don't manifest in this PR at all (added in an earlier commit and deleted here in the same PR).

Copy link
Contributor

@micahlee micahlee left a comment

Choose a reason for hiding this comment

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

Thanks @jvanderhoof, I left a few more comments on the PR as a whole. Feel free to take them or leave them! 😃

@@ -14,7 +20,10 @@ def find_all(type:, account:)
"#{account}:webservice:conjur/#{type}/%"
)
).all.map do |webservice|
load_authenticator(account: account, id: webservice.id.split(':').last, type: type)
service_id = service_id_from_id(webservice.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be service_id_from_resource_id instead?

@@ -14,7 +20,10 @@ def find_all(type:, account:)
"#{account}:webservice:conjur/#{type}/%"
)
).all.map do |webservice|
load_authenticator(account: account, id: webservice.id.split(':').last, type: type)
service_id = service_id_from_id(webservice.id)
next unless webservice.id.split(':').last == "conjur/#{type}/#{service_id}"
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not clear what business logic this is performing. Can you add a comment elaborating what the outcome of this filtering is?

id_token,
discovery_information.jwks
)
rescue Exception => e
Copy link
Contributor

Choose a reason for hiding this comment

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

Code climate also flags this. Do we need to rescue Exception here, or can it be just StandardError? If Exception is intentional, mind adding a comment why that is?

@logger = logger
end

# Don't love this name...
Copy link
Contributor

Choose a reason for hiding this comment

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

Mind either removing this comment, or elaborate on it such that someone other than you (or future you!) could continue the train of thought in an actionable way?

Comment on lines 253 to 262
# TODO - this is related to the feature flag 'pkce_support'. Once
# this feature is permanently added, this error should be removed.
StateMismatch = ::Util::TrackableErrorClass.new(
msg: "Conjur internal state doesn't match given state",
code: "CONJ00127E"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not clear to me why this is related to the feature flag itself. Mind elaborating a little more as to why this error won't be relevant once the feature is graduated from a flag?

@jvanderhoof jvanderhoof force-pushed the pkce-support branch 3 times, most recently from 0ebf0ea to 38d9d4e Compare December 6, 2022 20:27
CHANGELOG.md Outdated
@@ -9,7 +9,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Nothing should go in this section, please add to the latest unreleased version
(and update the corresponding date), or add a new version.

## [1.19.0] - 2022-11-29
## [1.19.0] - 2022-12-06
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this should be bumped to 1.19.1 (or greater).

full_id.split('/')[2]
end

def load_authenticator(type:, account:, service_id:)
Copy link

Choose a reason for hiding this comment

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

Method load_authenticator has 26 lines of code (exceeds 25 allowed). Consider refactoring.

code: "CONJ00133E"
)

# TODO - this error comes from the current version of the OIDC
Copy link

Choose a reason for hiding this comment

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

Annotation keywords like TODO should be all upper case, followed by a colon, and a space, then a note describing the problem.

:response_type
)

def initialize(
Copy link

Choose a reason for hiding this comment

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

Avoid parameter lists longer than 7 parameters. [10/7]

id_token,
discovery_information.jwks
)
rescue Exception => e
Copy link

Choose a reason for hiding this comment

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

Avoid rescuing the Exception class. Perhaps you meant to rescue StandardError?

begin
if @pkce_support_enabled
allowed_args = %i[account service_id] +
@data_object.const_get(:REQUIRED_VARIABLES) +
Copy link

Choose a reason for hiding this comment

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

Use 2 (not 14) spaces for indenting an expression in an assignment spanning multiple lines.

@szh
Copy link
Contributor

szh commented Dec 8, 2022

@jvanderhoof Is there an ETA for when this will be merged?

module AuthnOidc
module V2
class Status
def call(authenticator:)
Copy link

Choose a reason for hiding this comment

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

Unused method argument - authenticator. You can also write as call(*) if you want the method to accept any arguments but don't care about them.

@@ -56,7 +56,7 @@ def callback(code:)
id_token,
discovery_information.jwks
)
rescue Exception => e
rescue StandardError => e
Copy link

Choose a reason for hiding this comment

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

Omit the error class when rescuing StandardError by itself.

id_token,
discovery_information.jwks
)
rescue StandardError => e
Copy link

Choose a reason for hiding this comment

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

Omit the error class when rescuing StandardError by itself.

@@ -14,7 +20,14 @@ def find_all(type:, account:)
"#{account}:webservice:conjur/#{type}/%"
)
).all.map do |webservice|
load_authenticator(account: account, id: webservice.id.split(':').last, type: type)
service_id = service_id_from_resource_id(webservice.id)
Copy link

Choose a reason for hiding this comment

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

DB::Repository::AuthenticatorRepository#find_all calls 'webservice.id' 2 times

@@ -36,8 +49,12 @@

private

def load_authenticator(type:, account:, id:)
service_id = id.split('/')[2]
def service_id_from_resource_id(id)
Copy link

Choose a reason for hiding this comment

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

DB::Repository::AuthenticatorRepository#service_id_from_resource_id doesn't depend on instance state (maybe move it to another class?)

This commit fixes the issue where an OIDC authenticator with a status webservice
causes the providers endpoint to display the authenticator twice.
This commit adds support for PKCE to improve the security posture of OIDC Authentication. It also includes
small improvements to make the Authentication Handler and Authenticator Repository more generic and
durable.

This feature is behind a feature flag as the UI requires changes to support this improved workflow.
Copy link
Contributor

@micahlee micahlee left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks, @jvanderhoof

if @pkce_support_enabled
allowed_args = %i[account service_id] +
@data_object.const_get(:REQUIRED_VARIABLES) +
@data_object.const_get(:OPTIONAL_VARIABLES)
Copy link

Choose a reason for hiding this comment

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

Use 2 (not 14) spaces for indenting an expression in an assignment spanning multiple lines.

# the original OIDC authenticator is really a
# glorified JWT authenticator.
'Authentication::AuthnOidc::V2'
if pkce_support_enabled
Copy link

Choose a reason for hiding this comment

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

Authentication::Util::NamespaceSelector#self.select is controlled by argument 'pkce_support_enabled'

module AuthnOidc
module V2
class Status
def call(authenticator:)
Copy link

Choose a reason for hiding this comment

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

Authentication::AuthnOidc::V2::Status#call has unused parameter 'authenticator'

decoded_id_token
end

def discovery_information(invalidate: false)
Copy link

Choose a reason for hiding this comment

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

Authentication::AuthnOidc::PkceSupportFeature::Client#discovery_information has boolean parameter 'invalidate'

)
rescue Rack::OAuth2::Client::Error => e
# Only handle the expected errors related to access token retrieval.
case e.message
Copy link

Choose a reason for hiding this comment

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

Authentication::AuthnOidc::PkceSupportFeature::Client#callback calls 'e.message' 2 times

@codeclimate
Copy link

codeclimate bot commented Dec 8, 2022

Code Climate has analyzed commit 0951249 and detected 46 issues on this pull request.

Here's the issue category breakdown:

Category Count
Complexity 33
Duplication 4
Bug Risk 1
Style 8

The test coverage on the diff in this pull request is 77.5% (50% is the threshold).

This pull request will bring the total coverage in the repository to 90.0% (-1.7% change).

View more on Code Climate.

@jvanderhoof jvanderhoof merged commit 94aa6e9 into master Dec 9, 2022
@jvanderhoof jvanderhoof deleted the pkce-support branch December 9, 2022 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants