-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
113b626
commit 235ce06
Showing
28 changed files
with
2,090 additions
and
184 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
app/domain/authentication/authn_oidc/pkce_support_feature/client.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
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:) | ||
oidc_client.authorization_code = code | ||
begin | ||
bearer_token = oidc_client.access_token!( | ||
scope: true, | ||
client_auth_method: :basic, | ||
code_verifier: code_verifier | ||
) | ||
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 |
60 changes: 60 additions & 0 deletions
60
app/domain/authentication/authn_oidc/pkce_support_feature/data_objects/authenticator.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
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].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 | ||
) | ||
@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 | ||
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 | ||
end | ||
end | ||
end | ||
end | ||
end |
19 changes: 19 additions & 0 deletions
19
app/domain/authentication/authn_oidc/pkce_support_feature/resolve_identity.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
42 changes: 42 additions & 0 deletions
42
app/domain/authentication/authn_oidc/pkce_support_feature/strategy.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 code_verifier].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 |
60 changes: 60 additions & 0 deletions
60
app/domain/authentication/authn_oidc/pkce_support_feature/views/provider_context.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
require 'securerandom' | ||
require 'digest' | ||
|
||
module Authentication | ||
module AuthnOidc | ||
module PkceSupportFeature | ||
module Views | ||
class ProviderContext | ||
def initialize( | ||
client: Authentication::AuthnOidc::V2::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| | ||
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 | ||
) | ||
} | ||
end | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
module Authentication | ||
module AuthnOidc | ||
module V2 | ||
class Status | ||
def call(authenticator:) | ||
{ | ||
|
||
} | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.