-
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.
Revert "Remove support for disabling the non-PKCE OIDC authentication…
… flow" This reverts commit 753e359.
- Loading branch information
1 parent
753e359
commit 6e2ed7e
Showing
25 changed files
with
1,300 additions
and
467 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
115 changes: 115 additions & 0 deletions
115
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,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 |
69 changes: 69 additions & 0 deletions
69
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,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 |
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].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 |
66 changes: 66 additions & 0 deletions
66
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,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 |
Oops, something went wrong.