-
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.
- Loading branch information
Showing
6 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
24 changes: 24 additions & 0 deletions
24
app/domain/authentication/authn_jwt/signing_key/fetch_public_keys_signing_key.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,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 |
42 changes: 42 additions & 0 deletions
42
app/domain/authentication/authn_jwt/signing_key/public_signing_keys.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 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 |
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
102 changes: 102 additions & 0 deletions
102
spec/app/domain/authentication/authn-jwt/signing_key/fetch_public_keys_signing_key_spec.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,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 |
84 changes: 84 additions & 0 deletions
84
spec/app/domain/authentication/authn-jwt/signing_key/public_signing_keys_spec.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,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 |