Skip to content

Commit

Permalink
Interface updates to Authn-OIDC code redirect authentication flow
Browse files Browse the repository at this point in the history
This commit updates the previously implemented authn-oidc workflow to adhere to the small changes in interface
defined in the authn-jwt refactor
  • Loading branch information
jvanderhoof committed May 26, 2023
1 parent fa1fb4d commit 102c9d0
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 33 deletions.
18 changes: 16 additions & 2 deletions app/controllers/authenticate_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,24 @@ class AuthenticateController < ApplicationController
include BasicAuthenticator
include AuthorizeResource

def oidc_authenticate_code_redirect
def authenticate_via_get
handler = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator]
)
# TODO: need a mechanism for an authenticator strategy to define the required
# params. This will likely need to be done via the Handler.
params.permit!
params.permit(handler.params_allowed)

auth_token = handler.call(
parameters: params.to_hash.symbolize_keys,
request_ip: request.ip
)

render_authn_token(auth_token)
rescue => e
log_backtrace(e)
raise e
end

def authenticate_via_post
auth_token = Authentication::Handler::AuthenticationHandler.new(
Expand Down
19 changes: 12 additions & 7 deletions app/domain/authentication/authn_oidc/authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ def status(authenticator_status_input:)
# is done, the following check can be removed.

# Attempt to load the V2 version of the OIDC Authenticator
authenticator = DB::Repository::AuthenticatorRepository.new(
data_object: Authentication::AuthnOidc::V2::DataObjects::Authenticator
).find(
type: authenticator_status_input.authenticator_name,
account: authenticator_status_input.account,
service_id: authenticator_status_input.service_id
)
begin
authenticator = DB::Repository::AuthenticatorRepository.new(
data_object: Authentication::AuthnOidc::V2::DataObjects::Authenticator
).find(
type: authenticator_status_input.authenticator_name,
account: authenticator_status_input.account,
service_id: authenticator_status_input.service_id
)
rescue Errors::Conjur::RequiredSecretMissing
# If the authenticator we're looking for has missing variables, it may be that the user is
# after the original OIDC authenticator. Catch the error and use the old validator.
end
# If successful, validate the new set of required variables
if authenticator.present?
Authentication::AuthnOidc::ValidateStatus.new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def initialize(
@name = name
@provider_scope = provider_scope
@redirect_uri = redirect_uri
@token_ttl = token_ttl

# If variable is present but not set, token_ttl will come
# through as an empty string.
@token_ttl = token_ttl.present? ? token_ttl : 'PT8M'
end

def scope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Authentication
module AuthnOidc
module V2
module DataObjects

# This class handles all validation for the JWT authenticator. This contract
# is executed against the data gleaned from Conjur variables when the authenicator
# is loaded via the AuthenticatorRepository.

class AuthenticatorContract < Dry::Validation::Contract
option :utils

schema do
required(:account).value(:string)
required(:service_id).value(:string)
required(:provider_uri).value(:string)
required(:client_id).value(:string)
required(:client_secret).value(:string)
required(:claim_mapping).value(:string)

optional(:redirect_uri).value(:string)
optional(:response_type).value(:string)
optional(:provider_scope).value(:string)
optional(:name).value(:string)
optional(:token_ttl).value(:string)
end

# Verify that `provider_uri` has a secret value set if variable is present
rule(:provider_uri, :service_id, :account) do
if values[:provider_uri].empty?
utils.failed_response(
key: key,
error: Errors::Conjur::RequiredSecretMissing.new(
"#{values[:account]}:variable:conjur/authn-jwt/#{values[:service_id]}/provider-uri"
)
)
end
end
end
end
end
end
end
22 changes: 15 additions & 7 deletions app/domain/authentication/authn_oidc/v2/resolve_identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ module Authentication
module AuthnOidc
module V2
class ResolveIdentity
def call(identity:, account:, allowed_roles:)
# make sure role has a resource (ex. user, host)
roles = allowed_roles.select(&:resource?)
def initialize(authenticator:, logger: Rails.logger)
@authenticator = authenticator
@logger = logger
end

def call(identifier:, allowed_roles:, id: nil)
allowed_roles.each do |role|
next unless match?(identifier: identifier, role: role)

roles.each do |role|
role_account, _, role_id = role.id.split(':')
return role if role_account == account && identity == role_id
return role[:role_id]
end

raise(Errors::Authentication::Security::RoleNotFound, identity)
raise(Errors::Authentication::Security::RoleNotFound, identifier)
end

def match?(identifier:, role:)
role_account, _, role_id = role[:role_id].split(':')
role_account == @authenticator.account && identifier == role_id
end
end
end
Expand Down
16 changes: 10 additions & 6 deletions app/domain/authentication/authn_oidc/v2/strategy.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# frozen_string_literal: true

module Authentication
module AuthnOidc
module V2
class Strategy
REQUIRED_PARAMS = %i[code nonce].freeze

def initialize(
authenticator:,
client: Authentication::AuthnOidc::V2::Client,
Expand All @@ -12,19 +16,19 @@ def initialize(
@logger = logger
end

def callback(args)
def callback(parameters:, request_body: nil)
# NOTE: `code_verifier` param is optional
%i[code nonce].each do |param|
unless args[param].present?
REQUIRED_PARAMS.each do |param|
unless parameters[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]
code: parameters[:code],
nonce: parameters[:nonce],
code_verifier: parameters[:code_verifier]
)
)
unless identity.present?
Expand Down
4 changes: 2 additions & 2 deletions ci/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ services:
RAILS_ENV:
REQUIRE_SIMPLECOV: "true"
CONJUR_LOG_LEVEL: debug
CONJUR_AUTHENTICATORS: authn-ldap/test,authn-ldap/secure,authn-oidc/keycloak,authn-oidc,authn-k8s/test,authn-azure/prod,authn-gcp,authn-jwt/raw,authn-jwt/keycloak,authn-oidc/keycloak2,authn-oidc/okta-2
CONJUR_AUTHENTICATORS: authn-ldap/test,authn-ldap/secure,authn-oidc/keycloak,authn-oidc,authn-k8s/test,authn-azure/prod,authn-gcp,authn-jwt/raw,authn-jwt/keycloak,authn-oidc/keycloak2,authn-oidc/okta,authn-oidc/okta-2,authn-oidc/keycloak2-long-lived
LDAP_URI: ldap://ldap-server:389
LDAP_BASE: dc=conjur,dc=net
LDAP_FILTER: '(uid=%s)'
Expand Down Expand Up @@ -169,4 +169,4 @@ services:
volumes:
authn-local:
log-volume:
jwks-volume:
jwks-volume:
2 changes: 1 addition & 1 deletion dev/start
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ enable_oidc_authenticators() {
echo "Configuring Keycloak as OpenID provider for manual testing"
# We enable an OIDC authenticator without a service-id to test that it's
# invalid.
enabled_authenticators="$enabled_authenticators,authn-oidc/keycloak,authn-oidc,authn-oidc/keycloak2"
enabled_authenticators="$enabled_authenticators,authn-oidc/keycloak,authn-oidc,authn-oidc/keycloak2,authn-oidc/keycloak2-long-lived"
fi

if [[ $ENABLE_OIDC_OKTA = true ]]; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,20 @@

describe '.token_ttl', type: 'unit' do
context 'with default initializer' do
it { expect(authenticator.token_ttl).to eq(8.minutes) }
it { expect(authenticator.token_ttl).to eq(60.minutes) }
end

context 'when initialized with a valid duration' do
let (:args) { default_args.merge({ token_ttl: 'PT1H'}) }
it { expect(authenticator.token_ttl).to eq(1.hour)}
let(:args) { default_args.merge({ token_ttl: 'PT2H' }) }
it { expect(authenticator.token_ttl).to eq(2.hours)}
end

context 'when initialized with an invalid duration' do
let (:args) { default_args.merge({ token_ttl: 'PTinvalidH' }) }
it { expect {
authenticator.token_ttl
}.to raise_error(Errors::Authentication::DataObjects::InvalidTokenTTL) }
let(:args) { default_args.merge({ token_ttl: 'PTinvalidH' }) }
it {
expect { authenticator.token_ttl }
.to raise_error(Errors::Authentication::DataObjects::InvalidTokenTTL)
}
end
end
end

0 comments on commit 102c9d0

Please sign in to comment.