Skip to content

Commit

Permalink
Parser for public-keys value
Browse files Browse the repository at this point in the history
  • Loading branch information
sashaCher committed Dec 29, 2021
1 parent 2c93793 commit 32412a3
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

module Authentication
module AuthnJwt
module SigningKey
# This class is responsible for parsing JWK set from public-keys configuration value
class FetchPublicKeysSigningKey

def initialize(
public_keys:,
logger: Rails.logger
)
@logger = logger
@public_keys = public_keys
end

def call(force_fetch:)
signing_keys = PublicSigningKeys.new(JSON.parse(@public_keys))
signing_keys.validate!
{ keys: JSON::JWK::Set.new(signing_keys.value) }
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

module Authentication
module AuthnJwt
module SigningKey
# This class is a POJO class presents public-keys structure
class PublicSigningKeys
include ActiveModel::Validations, AttrRequired

VALID_TYPES = %w(jwks).freeze

attr_required(:type, :value)

validates *required_attributes, presence: true
validates :type, inclusion: { in: VALID_TYPES, message: "'%{value}' is not a valid public-keys type" }
validate :validate_value_is_jwks, if: -> { @type == "jwks" }

def initialize(hash)
raise Errors::Authentication::AuthnJwt::InvalidPublicKeys.new("the value is not in valid JSON format") unless
hash.is_a?(Hash)
hash = hash.with_indifferent_access
required_attributes.each do |key|
self.send "#{key}=", hash[key]
end
end

def validate!
valid? or raise Errors::Authentication::AuthnJwt::InvalidPublicKeys.new(errors.full_messages.to_sentence)
end

private

def validate_value_is_jwks
errors.add :value, "is not a valid JWKS (RFC7517)." unless
@value.is_a?(Hash) &&
@value.has_key?(:keys) &&
@value[:keys].is_a?(Array) &&
!@value[:keys].empty?
end
end
end
end
end
5 changes: 5 additions & 0 deletions app/domain/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,11 @@ module AuthnJwt
msg: "Restriction '{0-restriction-name}' is invalid and not representing claim path in the token",
code: "CONJ00119E"
)

InvalidPublicKeys = ::Util::TrackableErrorClass.new(
msg: "Failed to parse 'public-keys': {0-parse-error}",
code: "CONJ00120E"
)
end

module ResourceRestrictions
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.
en:
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# frozen_string_literal: true

require 'spec_helper'
require 'json'
require 'net/http'

RSpec.describe('Authentication::AuthnJwt::SigningKey::FetchPublicKeysSigningKey') do

let(:authenticator_name) { "authn-jwt" }
let(:service_id) { "my-service" }
let(:account) { "my-account" }
let(:mocked_authenticator_input) {
Authentication::AuthenticatorInput.new(
authenticator_name: authenticator_name,
service_id: service_id,
account: account,
username: "dummy_identity",
credentials: "dummy",
client_ip: "dummy",
request: "dummy"
)
}

let(:required_jwks_uri_configuration_error) { "required jwks_uri configuration missing error" }
let(:bad_response_error) { "bad response error" }
let(:required_secret_missing_error) { "required secret missing error" }
let(:mocked_logger) { double("Mocked Logger") }
let(:mocked_fetch_signing_key) { double("MockedFetchSigningKey") }
let(:mocked_fetch_signing_key_refresh_value) { double("MockedFetchSigningKeyRefreshValue") }
let(:mocked_fetch_authenticator_secrets_exist_values) { double("MockedFetchAuthenticatorSecrets") }
let(:mocked_fetch_authenticator_secrets_empty_values) { double("MockedFetchAuthenticatorSecrets") }
let(:mocked_bad_http_response) { double("Mocked bad http response") }
let(:mocked_good_http_response) { double("Mocked good http response") }
let(:mocked_bad_response) { double("Mocked bad http body") }
let(:mocked_good_response) { double("Mocked good http body") }
let(:mocked_create_jwks_from_http_response) { double("Mocked good jwks") }

let(:good_response) { "good-response"}
let(:bad_response) { "bad-response"}
let(:valid_jwks) { "valid-jwls" }

before(:each) do
allow(mocked_logger).to(
receive(:call).and_return(true)
)

allow(mocked_logger).to(
receive(:debug).and_return(true)
)

allow(mocked_logger).to(
receive(:info).and_return(true)
)

allow(mocked_fetch_signing_key).to receive(:call) { |params| params[:signing_key_provider].fetch_signing_key }
allow(mocked_fetch_signing_key_refresh_value).to receive(:call) { |params| params[:refresh] }

allow(mocked_fetch_authenticator_secrets_exist_values).to(
receive(:call).and_return('jwks-uri' => 'https://jwks-uri.com/jwks')
)

allow(mocked_fetch_authenticator_secrets_empty_values).to(
receive(:call).and_raise(required_secret_missing_error)
)

allow(mocked_bad_http_response).to(
receive(:get_response).and_return(bad_response)
)

allow(mocked_good_http_response).to(
receive(:get_response).and_return(good_response)
)

allow(mocked_create_jwks_from_http_response).to(
receive(:call).with(http_response: good_response).and_return(valid_jwks)
)

allow(mocked_create_jwks_from_http_response).to(
receive(:call).with(http_response: bad_response).and_raise(bad_response_error)
)
end

# ____ _ _ ____ ____ ____ ___ ____ ___
# (_ _)( )_( )( ___) (_ _)( ___)/ __)(_ _)/ __)
# )( ) _ ( )__) )( )__) \__ \ )( \__ \
# (__) (_) (_)(____) (__) (____)(___/ (__) (___/

context "FetchPublicKeysSigningKey call " do
context "propagates false refresh value" do
subject do
jwks = Net::HTTP.get_response(URI("https://www.googleapis.com/oauth2/v3/certs")).body
jwks = "{\"type\":\"jwks\", \"value\": {\"kuku\":\"shmuku\", \"keys\":[\"ddd\"]}}"
::Authentication::AuthnJwt::SigningKey::FetchPublicKeysSigningKey.new(public_keys: jwks
).call(force_fetch: false)
end

it "returns false" do
expect(subject).to eql(false)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe('Authentication::AuthnJwt::SigningKey::PublicSigningKeys') do

invalid_cases = {
"When public-keys value is string":
["blah",
"CONJ00120E Failed to parse 'public-keys': the value is not in valid JSON format"],
"When public-keys value is empty object":
[{},
"CONJ00120E Failed to parse 'public-keys': Type can't be blank, Type '' is not a valid public-keys type, and Value can't be blank"],
"When public-keys does not contain needed fields":
[{:key => "value", :key2 => { :key3 => "valve" }},
"CONJ00120E Failed to parse 'public-keys': Type can't be blank, Type '' is not a valid public-keys type, and Value can't be blank"],
"When public-keys type is empty and value is absent":
[{:type => ""},
"CONJ00120E Failed to parse 'public-keys': Type can't be blank, Type '' is not a valid public-keys type, and Value can't be blank"],
"When public-keys type has wrong value and value is absent":
[{:type => "yes"},
"CONJ00120E Failed to parse 'public-keys': Value can't be blank and Type 'yes' is not a valid public-keys type"]
}

# valid_cases = {
# "When claim name contains 1 allowed char 'F'": "F",
# "When claim name contains 1 allowed char 'f'": "f",
# "When claim name contains 1 allowed char '_'": "_",
# "When claim name contains value with allowed char '/'": "a/a",
# "When claim name contains value with multiple allowed chars '/'": "a/a/a/a",
# "When claim name contains 1 allowed char '$'": "$",
# "When claim name contains digits in the middle": "$2w",
# "When claim name contains dots in the middle": "$...4.w",
# "When claim name ends with dots": "$w...",
# "When claim name ends with digits": "$2w9",
# "When claim name contains allowed character '|'": "a|b"
# }
#
# deny_list_cases = {
# "When claim name value is 'exp'": "exp",
# "When claim name value is 'iat'": "iat",
# "When claim name value is 'nbf'": "nbf",
# "When claim name value is 'jti'": "jti",
# "When claim name value is 'aud'": "aud",
# "When claim name value is 'iss'": "iss"
# }
#
# not_in_deny_list_cases = {
# "When claim name value is 'sub'": "sub",
# "When claim name value is substring of forbidden claim 'exp1'": "exp1",
# "When claim name value is substring of forbidden claim '$exp'": "$exp"
# }

context "Input validation" do
context "Invalid examples" do
invalid_cases.each do |description, (hash, expected_error_message) |
context "#{description}" do
subject do
Authentication::AuthnJwt::SigningKey::PublicSigningKeys.new(hash)
end

it "raises an error" do

expect { subject.validate! }
.to raise_error(
Errors::Authentication::AuthnJwt::InvalidPublicKeys,
expected_error_message)
end
end
end
end

# context "Valid examples" do
# valid_cases.each do |description, claim_name|
# context "#{description}" do
# it "does not raise error" do
# expect { claim_name_validator.call(claim_name: claim_name) }.not_to raise_error
# end
# end
# end
# end

end
end

0 comments on commit 32412a3

Please sign in to comment.