-
Notifications
You must be signed in to change notification settings - Fork 124
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
Conversation
end | ||
end | ||
|
||
def callback(code:, nonce:, code_verifier:) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:) |
There was a problem hiding this comment.
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)
c188f13
to
adece22
Compare
end | ||
end | ||
|
||
def callback(code:, nonce:, code_verifier:) |
There was a problem hiding this comment.
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.
app/domain/errors.rb
Outdated
code: "CONJ00133E" | ||
) | ||
|
||
# TODO - this is related to the feature flag 'pkce_support'. Once |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doh! good catch.
adece22
to
fd6155b
Compare
There was a problem hiding this 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?)
spec/spec_helper.rb
Outdated
@@ -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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
if Rails.configuration.feature_flags.enabled?(:pkce_support) | ||
Authentication::AuthnOidc::PkceSupportFeature::DataObjects::Authenticator | ||
else | ||
Authentication::AuthnOidc::V2::DataObjects::Authenticator | ||
end |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
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. |
Hey @jvanderhoof, I think you meant to rebase this PR, rather than merge master in: |
Gemfile
Outdated
@@ -80,7 +80,6 @@ gem 'i18n', '~> 1.8.11' | |||
group :development, :test do | |||
gem 'aruba' | |||
gem 'ci_reporter_rspec' | |||
gem 'climate_control' |
There was a problem hiding this comment.
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).
There was a problem hiding this 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) |
There was a problem hiding this comment.
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}" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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... |
There was a problem hiding this comment.
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?
app/domain/errors.rb
Outdated
# 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" | ||
) |
There was a problem hiding this comment.
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?
0ebf0ea
to
38d9d4e
Compare
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 |
There was a problem hiding this comment.
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:) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) + |
There was a problem hiding this comment.
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.
@jvanderhoof Is there an ETA for when this will be merged? |
6ca3b52
to
235ce06
Compare
module AuthnOidc | ||
module V2 | ||
class Status | ||
def call(authenticator:) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?)
235ce06
to
c620d14
Compare
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.
c620d14
to
0951249
Compare
There was a problem hiding this 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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
Code Climate has analyzed commit 0951249 and detected 46 issues on this pull request. Here's the issue category breakdown:
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. |
Desired Outcome
This PR attempts to accomplish two objectives:
Implemented Changes
PKCE Support
Authenticator
data object. This allows the repository to load only relevant variables.Feature Flag Patterning
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
CHANGELOG update
Test coverage
changes, or
Documentation
README
s) were updated in this PRBehavior
Security