Skip to content

Commit

Permalink
changelog: Upcoming Features, Attempts API, Adding signature validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Sgtpluck committed Feb 18, 2025
1 parent 71e98b6 commit 3a58052
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 3 deletions.
56 changes: 56 additions & 0 deletions app/controllers/api/attempts/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class EventsController < ApplicationController
prepend_before_action :skip_session_load
prepend_before_action :skip_session_expiration

skip_before_action :verify_authenticity_token
before_action :authenticate_client, only: :poll

def poll
head :method_not_allowed
end
Expand All @@ -19,6 +22,59 @@ def status
reason: :not_yet_implemented,
}
end

private

def authenticate_client
bearer, issuer, token = request.authorization&.split(' ', 3)
if bearer != 'Bearer' ||
config_data(issuer).blank? || !valid_auth_token?(token, issuer)

render json: { status: 401, description: 'Unauthorized' }, status: :unauthorized
end
end

def service_provider(issuer)
!IdentityConfig.store.allowed_attempts_providers.map(&:issuer).include?(issuer)
end

def valid_auth_token?(token, issuer)
begin
pub_key = OpenSSL::X509::Certificate.new(
config_data(issuer)[:key],
).public_key
rescue OpenSSL::X509::CertificateError
return false
end

begin
token_payload, _headers = JWT.decode(
token,
pub_key,
true,
algorithm: 'RS256',
)
rescue JWT::VerificationError
return false
end

payload_matches?(token_payload)
end

def payload_matches?(token_payload)
hashed_payload = Digest::SHA256.hexdigest(poll_params.to_json)
token_payload == hashed_payload
end

def poll_params
params.permit(:maxEvents, acks: [])
end

def config_data(issuer)
IdentityConfig.store.allowed_attempts_providers.find do |config|
config[:issuer] == issuer
end
end
end
end
end
1 change: 1 addition & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ acuant_sdk_initialization_endpoint: 'https://us.acas.acuant.net'
add_email_link_valid_for_hours: 24
address_identity_proofing_supported_country_codes: '["AS", "GU", "MP", "PR", "US", "VI"]'
all_redirect_uris_cache_duration_minutes: 2
allowed_attempts_providers: '[]'
allowed_ialmax_providers: '[]'
allowed_verified_within_providers: '[]'
asset_host: ''
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def self.store
config.add(:add_email_link_valid_for_hours, type: :integer)
config.add(:address_identity_proofing_supported_country_codes, type: :json)
config.add(:all_redirect_uris_cache_duration_minutes, type: :integer)
config.add(:allowed_attempts_providers, type: :json)
config.add(:allowed_ialmax_providers, type: :json)
config.add(:allowed_verified_within_providers, type: :json)
config.add(:asset_host, type: :string)
Expand Down
115 changes: 112 additions & 3 deletions spec/controllers/api/attempts/events_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,61 @@
end

describe '#poll' do
let(:action) { post :poll }
let(:sp) { create(:service_provider) }
let(:issuer) { sp.issuer }
let(:payload) do
{
maxEvents: '1000',
acks: [
'acknowleded-jti-id-1',
'acknowleded-jti-id-2',
],
}
end

let(:private_key) { OpenSSL::PKey::RSA.new 2048 }
let(:public_key) { private_key.public_key }

let(:public_cert) do
return nil if !issuer
name = OpenSSL::X509::Name.parse('/CN=signing')

cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 0
cert.not_before = Time.zone.now
cert.not_after = Time.zone.now + 3600

cert.public_key = public_key
cert.subject = name
cert.issuer = name

cert.sign private_key, 'SHA256'

cert.to_pem
end

let(:token) do
JWT.encode(
Digest::SHA256.hexdigest(payload.to_json),
private_key,
'RS256',
)
end

let(:auth_header) { "Bearer #{issuer} #{token}" }

before do
request.headers['Authorization'] = auth_header
allow(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return(
[{
issuer: sp.issuer,
key: public_cert,
}],
)
end

let(:action) { post :poll, params: payload }

context 'when the Attempts API is not enabled' do
it 'returns 404 not found' do
Expand All @@ -19,8 +73,63 @@

context 'when the Attempts API is enabled' do
let(:enabled) { true }
it 'returns 405 method not allowed' do
expect(action.status).to eq(405)

context 'with a valid authorization header' do
it 'returns 405 method not allowed' do
expect(action.status).to eq(405)
end
end

context 'with an invalid authorization header' do
context 'with no Authorization header' do
let(:auth_header) { nil }

it 'returns a 401' do
expect(action.status).to eq 401
end
end

context 'when Authorization header is an empty string' do
let(:auth_header) { '' }

it 'returns a 401' do
expect(action.status).to eq 401
end
end

context 'without a Bearer token Authorization header' do
let(:auth_header) { "#{issuer} #{token}" }

it 'returns a 401' do
expect(action.status).to eq 401
end
end

context 'without a valid issuer' do
context 'an unknown issuer' do
let(:issuer) { 'random-issuer' }

it 'returns a 401' do
expect(action.status).to eq 401
end
end
end

context 'without a valid public key' do
let(:public_cert) { 'not-a-cert' }

it 'returns a 401' do
expect(action.status).to eq 401
end
end

context 'with a valid but not matching public key' do
let(:public_key) { OpenSSL::PKey::RSA.new(2048).public_key }

it 'returns a 401' do
expect(action.status).to eq 401
end
end
end
end
end
Expand Down

0 comments on commit 3a58052

Please sign in to comment.