Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
69 changed files
with
5,654 additions
and
68 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
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
22 changes: 22 additions & 0 deletions
22
app/domain/authentication/authn_jwt/authentication_parameters.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,22 @@ | ||
module Authentication | ||
module AuthnJwt | ||
# Data class to store data regarding jwt token that is needed during the jwt authentication process | ||
class AuthenticationParameters | ||
attr_accessor :decoded_token, :jwt_token | ||
attr_reader :authenticator_name, :service_id, :account, :username, :client_ip, :request | ||
|
||
def initialize(authentication_input:, jwt_token:) | ||
@authenticator_name = authentication_input.authenticator_name | ||
@service_id = authentication_input.service_id | ||
@account = authentication_input.account | ||
@username = authentication_input.username | ||
@client_ip = authentication_input.client_ip | ||
@jwt_token = jwt_token | ||
end | ||
|
||
def authn_jwt_variable_id_prefix | ||
"#{@account}:variable:conjur/#{@authenticator_name}/#{@service_id}" | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
require 'command_class' | ||
|
||
module Authentication | ||
module AuthnJwt | ||
# Generic JWT authenticator that receive JWT vendor configuration and uses to validate that the authentication | ||
# request is valid, and return conjur authn token accordingly | ||
Authenticator = CommandClass.new( | ||
dependencies: { | ||
token_factory: TokenFactory.new, | ||
logger: Rails.logger, | ||
audit_log: ::Audit.logger, | ||
validate_origin: ::Authentication::ValidateOrigin.new, | ||
role_class: ::Role, | ||
webservice_class: ::Authentication::Webservice, | ||
validate_role_can_access_webservice: ::Authentication::Security::ValidateRoleCanAccessWebservice.new, | ||
role_id_class: Audit::Event::Authn::RoleId | ||
}, | ||
inputs: %i[jwt_configuration authenticator_input] | ||
) do | ||
extend(Forwardable) | ||
def_delegators(:@authenticator_input, :account, :username, :client_ip, :authenticator_name, :service_id) | ||
|
||
def call | ||
validate_and_decode_token | ||
get_jwt_identity_from_request | ||
validate_host_has_access_to_webservice | ||
validate_origin | ||
validate_restrictions | ||
audit_success | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::JwtAuthenticationPassed.new) | ||
new_token | ||
rescue => e | ||
audit_failure(e) | ||
raise e | ||
end | ||
|
||
private | ||
|
||
def validate_and_decode_token | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::CallingValidateAndDecodeToken.new) | ||
@jwt_configuration.validate_and_decode_token | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::ValidateAndDecodeTokenPassed.new) | ||
end | ||
|
||
def get_jwt_identity_from_request | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::CallingGetJwtIdentity.new) | ||
jwt_identity | ||
@logger.info(LogMessages::Authentication::AuthnJwt::FoundJwtIdentity.new(jwt_identity)) | ||
end | ||
|
||
def jwt_identity | ||
@jwt_identity ||= @jwt_configuration.jwt_identity | ||
end | ||
|
||
def validate_host_has_access_to_webservice | ||
@validate_role_can_access_webservice.( | ||
webservice: webservice, | ||
account: account, | ||
user_id: jwt_identity, | ||
privilege: PRIVILEGE_AUTHENTICATE | ||
) | ||
end | ||
|
||
def validate_origin | ||
@validate_origin.( | ||
account: account, | ||
username: jwt_identity, | ||
client_ip: client_ip | ||
) | ||
end | ||
|
||
def validate_restrictions | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::CallingValidateRestrictions.new) | ||
@jwt_configuration.validate_restrictions | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::ValidateRestrictionsPassed.new) | ||
end | ||
|
||
def audit_success | ||
@audit_log.log( | ||
::Audit::Event::Authn::Authenticate.new( | ||
authenticator_name: authenticator_name, | ||
service: webservice, | ||
role_id: audit_role_id, | ||
client_ip: client_ip, | ||
success: true, | ||
error_message: nil | ||
) | ||
) | ||
end | ||
|
||
def audit_failure(err) | ||
@audit_log.log( | ||
::Audit::Event::Authn::Authenticate.new( | ||
authenticator_name: authenticator_name, | ||
service: webservice, | ||
role_id: audit_role_id, | ||
client_ip: client_ip, | ||
success: false, | ||
error_message: err.message | ||
) | ||
) | ||
end | ||
|
||
def identity_role | ||
@identity_role ||= @role_class.by_login( | ||
jwt_identity, | ||
account: account | ||
) | ||
end | ||
|
||
# If there is no jwt identity so role and username are nil | ||
def audit_role_id | ||
return @audit_role_id if @audit_role_id | ||
# We use '@jwt_identity' and not 'jwt_identity' so that we don't call the function in case 'validate_and_decode' | ||
# failed. In such a case, we want to still be able to log an audit message without the role and username. | ||
if @jwt_identity | ||
role = identity_role | ||
username = jwt_identity | ||
end | ||
@audit_role_id = @role_id_class.new( | ||
role: role, | ||
account: account, | ||
username: username | ||
).to_s | ||
end | ||
|
||
def webservice | ||
@webservice ||= @webservice_class.new( | ||
account: account, | ||
authenticator_name: authenticator_name, | ||
service_id: service_id | ||
) | ||
end | ||
|
||
def new_token | ||
@token_factory.signed_token( | ||
account: account, | ||
username: jwt_identity | ||
) | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# frozen_string_literal: true | ||
|
||
module Authentication | ||
module AuthnJwt | ||
PROVIDER_URI_RESOURCE_NAME = "provider-uri" | ||
JWKS_URI_RESOURCE_NAME = "jwks-uri" | ||
PROVIDER_URI_INTERFACE_NAME = PROVIDER_URI_RESOURCE_NAME.freeze | ||
JWKS_URI_INTERFACE_NAME = JWKS_URI_RESOURCE_NAME.freeze | ||
ISSUER_RESOURCE_NAME = "issuer" | ||
TOKEN_APP_PROPERTY_VARIABLE = "token-app-property" | ||
IDENTITY_NOT_RETRIEVED_YET = "Identity not retrieved yet" | ||
URL_IDENTITY_PROVIDER_INTERFACE_NAME = "url-identity-provider" | ||
TOKEN_IDENTITY_PROVIDER_INTERFACE_NAME = "token-identity-provider" | ||
PRIVILEGE_AUTHENTICATE="authenticate" | ||
ISS_CLAIM_NAME = "iss" | ||
EXP_CLAIM_NAME = "exp" | ||
NBF_CLAIM_NAME = "nbf" | ||
IAT_CLAIM_NAME = "iat" | ||
RSA_ALGORITHMS = %w[RS256 RS384 RS512].freeze | ||
ECDSA_ALGORITHMS = %w[ES256 ES384 ES512].freeze | ||
SUPPORTED_ALGORITHMS = (RSA_ALGORITHMS + ECDSA_ALGORITHMS) | ||
CACHE_REFRESHES_PER_INTERVAL = 10 | ||
CACHE_RATE_LIMIT_INTERVAL = 300 | ||
CACHE_MAX_CONCURRENT_REQUESTS = 3 | ||
MANDATORY_CLAIMS = [EXP_CLAIM_NAME].freeze | ||
OPTIONAL_CLAIMS = [ISS_CLAIM_NAME, NBF_CLAIM_NAME, IAT_CLAIM_NAME].freeze | ||
end | ||
end |
74 changes: 74 additions & 0 deletions
74
app/domain/authentication/authn_jwt/identity_providers/create_identity_provider.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,74 @@ | ||
require 'command_class' | ||
|
||
module Authentication | ||
module AuthnJwt | ||
module IdentityProviders | ||
# Factory for jwt identity providers. | ||
# If Identity variable is configured factory return the decoded_token_provider | ||
# If the Identity variable is not configured and there is account field in url factory returns url provider | ||
# If the above conditions are not met exception is raised | ||
CreateIdentityProvider = CommandClass.new( | ||
dependencies: { | ||
identity_from_url_provider_class: Authentication::AuthnJwt::IdentityProviders::IdentityFromUrlProvider, | ||
identity_from_decoded_token_class: Authentication::AuthnJwt::IdentityProviders::IdentityFromDecodedTokenProvider, | ||
logger: Rails.logger | ||
}, | ||
inputs: %i[authentication_parameters] | ||
) do | ||
|
||
def call | ||
validate_identity_configuration | ||
create_identity_provider | ||
end | ||
|
||
private | ||
|
||
def create_identity_provider | ||
@logger.debug(LogMessages::Authentication::AuthnJwt::SelectingIdentityProviderInterface.new) | ||
|
||
if identity_from_decoded_token_provider.identity_available? | ||
@logger.info( | ||
LogMessages::Authentication::AuthnJwt::SelectedIdentityProviderInterface.new( | ||
TOKEN_IDENTITY_PROVIDER_INTERFACE_NAME | ||
) | ||
) | ||
identity_from_decoded_token_provider | ||
elsif identity_from_url_provider.identity_available? | ||
@logger.info( | ||
LogMessages::Authentication::AuthnJwt::SelectedIdentityProviderInterface.new( | ||
URL_IDENTITY_PROVIDER_INTERFACE_NAME | ||
) | ||
) | ||
identity_from_url_provider | ||
end | ||
end | ||
|
||
def identity_from_decoded_token_provider | ||
@identity_from_decoded_token_provider ||= @identity_from_decoded_token_class.new( | ||
authentication_parameters: @authentication_parameters | ||
) | ||
end | ||
|
||
def identity_from_url_provider | ||
@identity_from_url_provider ||= @identity_from_url_provider_class.new( | ||
authentication_parameters: @authentication_parameters | ||
) | ||
end | ||
|
||
def validate_identity_configuration | ||
if multiple_identities_configured || no_identities_configured | ||
raise Errors::Authentication::AuthnJwt::IdentityMisconfigured | ||
end | ||
end | ||
|
||
def multiple_identities_configured | ||
identity_from_decoded_token_provider.identity_available? && identity_from_url_provider.identity_available? | ||
end | ||
|
||
def no_identities_configured | ||
!identity_from_decoded_token_provider.identity_available? && !identity_from_url_provider.identity_available? | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.