Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ def analytics_user
current_user || AnonymousUser.new
end

def attempts_api_tracker
@attempts_api_tracker ||= AttemptsApi::Tracker.new(
session_id: attempts_api_session_id,
request:,
user: current_user,
sp: current_sp,
cookie_device_uuid: cookies[:device],
# this only works for oidc
sp_request_uri: decorated_sp_session.request_url_params[:redirect_uri],
enabled_for_session: attempts_api_enabled_for_session?,
)
end

def user_event_creator
@user_event_creator ||= UserEventCreator.new(request: request, current_user: current_user)
end
Expand Down Expand Up @@ -127,6 +140,14 @@ def current_sp

private

def attempts_api_enabled_for_session?
current_sp&.attempts_api_enabled? && attempts_api_session_id.present?
end

def attempts_api_session_id
@attempts_api_session_id ||= decorated_sp_session.attempts_api_session_id
end

# These attributes show up in New Relic traces for all requests.
# https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/collect-custom-attributes
def add_new_relic_trace_attributes
Expand Down
1 change: 1 addition & 0 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ def track_authentication_attempt
remember_device: remember_device_cookie.present?,
new_device: success ? new_device? : nil,
)
attempts_api_tracker.email_and_password_auth(success:)
end

def user_locked_out?(user)
Expand Down
2 changes: 2 additions & 0 deletions app/decorators/null_service_provider_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def current_user
view_context&.current_user
end

def attempts_api_session_id; end

private

attr_reader :view_context
Expand Down
4 changes: 4 additions & 0 deletions app/decorators/service_provider_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ def initialize(sp:, view_context:, sp_session:, service_provider_request:)
@service_provider_request = service_provider_request
end

def attempts_api_session_id
request_url_params['attempts_api_session_id']
end

def remember_device_default
sp_aal < 2
end
Expand Down
18 changes: 18 additions & 0 deletions app/models/service_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,26 @@ def facial_match_ial_allowed?
IdentityConfig.store.facial_match_general_availability_enabled
end

def attempts_api_enabled?
IdentityConfig.store.attempts_api_enabled && attempts_config.present?
end

def attempts_public_key
if attempts_config.present? && attempts_config['keys'].present?
OpenSSL::PKey::RSA.new(attempts_config['keys'].first)
else
ssl_certs.first.public_key
end
end

private

def attempts_config
IdentityConfig.store.allowed_attempts_providers.find do |config|
config['issuer'] == issuer
end
end

# @return [String,nil]
def load_cert(cert)
if cert.include?('-----BEGIN CERTIFICATE-----')
Expand Down
8 changes: 0 additions & 8 deletions app/services/attempts_api/request_token_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ def config_data_exists
)
end

def issuer_is_authorized
errors.add(
:issuer,
:not_authorized,
message: 'Issuer is not authorized to use Attempts API',
)
end

def service_provider_exists
return if service_provider.present?

Expand Down
7 changes: 3 additions & 4 deletions app/services/attempts_api/tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
module AttemptsApi
class Tracker
attr_reader :session_id, :enabled_for_session, :request, :user, :sp, :cookie_device_uuid,
:sp_request_uri, :analytics
:sp_request_uri

def initialize(session_id:, request:, user:, sp:, cookie_device_uuid:,
sp_request_uri:, enabled_for_session:, analytics:)
sp_request_uri:, enabled_for_session:)
@session_id = session_id
@request = request
@user = user
@sp = sp
@cookie_device_uuid = cookie_device_uuid
@sp_request_uri = sp_request_uri
@enabled_for_session = enabled_for_session
@analytics = analytics
end
include TrackerEvents

Expand Down Expand Up @@ -50,7 +49,7 @@ def track_event(event_type, metadata = {})

jwe = event.to_jwe(
issuer: sp.issuer,
public_key: sp.ssl_certs.first.public_key,
public_key: sp.attempts_public_key,
)

redis_client.write_event(
Expand Down
4 changes: 1 addition & 3 deletions app/services/attempts_api/tracker_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

module AttemptsApi
module TrackerEvents
# @param [String] email The submitted email address
# @param [Boolean] success True if the email and password matched
# A user has submitted an email address and password for authentication
def email_and_password_auth(email:, success:)
def email_and_password_auth(success:)
track_event(
'login-email-and-password-auth',
email: email,
success: success,
)
end
Expand Down
80 changes: 80 additions & 0 deletions spec/controllers/application_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,86 @@ def index
end
end

describe '#attempts_api_tracker' do
let(:enabled) { true }
let(:sp) { create(:service_provider) }
let(:user) { create(:user) }

before do
expect(IdentityConfig.store).to receive(:attempts_api_enabled).and_return enabled
allow(controller).to receive(:current_user).and_return(user)
allow(controller).to receive(:current_sp).and_return(sp)
end

context 'when the attempts api is not enabled' do
let(:enabled) { false }

it 'calls the AttemptsApi::Tracker class with enabled_for_session set to false' do
expect(AttemptsApi::Tracker).to receive(:new).with(
user:, request:, sp:, session_id: nil,
cookie_device_uuid: nil, sp_request_uri: nil, enabled_for_session: false
)

controller.attempts_api_tracker
end
end

context 'when attempts api is enabled' do
context 'when the service provider is not authorized' do
before do
expect(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return([])
end

it 'calls the AttemptsApi::Tracker class with enabled_for_session set to false' do
expect(AttemptsApi::Tracker).to receive(:new).with(
user:, request:, sp:, session_id: nil,
cookie_device_uuid: nil, sp_request_uri: nil, enabled_for_session: false
)

controller.attempts_api_tracker
end
end

context 'when the service provider is authorized' do
before do
expect(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return(
[
{
'issuer' => sp.issuer,
},
],
)
end

context 'when there is no attempts_api_session_id' do
it 'calls the AttemptsApi::Tracker class with enabled_for_session set to false' do
expect(AttemptsApi::Tracker).to receive(:new).with(
user:, request:, sp:, session_id: nil,
cookie_device_uuid: nil, sp_request_uri: nil, enabled_for_session: false
)

controller.attempts_api_tracker
end
end

context 'when there is an attempts_api_session_id' do
before do
expect(controller.decorated_sp_session).to receive(:attempts_api_session_id)
.and_return('abc123')
end
it 'calls the AttemptsApi::Tracker class with enabled_for_session set to true' do
expect(AttemptsApi::Tracker).to receive(:new).with(
user:, request:, sp:, session_id: 'abc123',
cookie_device_uuid: nil, sp_request_uri: nil, enabled_for_session: true
)

controller.attempts_api_tracker
end
end
end
end
end

def expect_user_event_to_have_been_created(user, event_type)
device = Device.first
expect(device.user_id).to eq(user.id)
Expand Down
6 changes: 6 additions & 0 deletions spec/decorators/null_service_provider_session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
end
end

describe '#attempts_api_session_id' do
it 'returns nil' do
expect(subject.attempts_api_session_id).to be_nil
end
end

describe '#cancel_link_url' do
it 'returns view_context.root url' do
view_context = ActionController::Base.new.view_context
Expand Down
20 changes: 20 additions & 0 deletions spec/decorators/service_provider_session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,24 @@
end
end
end

describe '#attempt_api_session_id' do
let(:service_provider_request) { ServiceProviderRequest.new(url:) }

context 'without an an attempts_api_session_id in the request_url_params' do
let(:url) { 'https://example.com/auth?param0=p0&param1=p1&param2=p2' }

it 'returns nil' do
expect(subject.attempts_api_session_id).to be nil
end
end

context 'with an attempts_api_session_id in the request_url_params' do
let(:url) { 'https://example.com/auth?attempts_api_session_id=abc123&param1=p1&param2=p2' }

it 'returns the value in the attempts_api_session_id param' do
expect(subject.attempts_api_session_id).to eq 'abc123'
end
end
end
end
91 changes: 91 additions & 0 deletions spec/models/service_provider_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,97 @@
end
end

describe '#attempts_api_enabled?' do
context 'when attempts api is enabled' do
before do
allow(IdentityConfig.store).to receive(:attempts_api_enabled)
.and_return(true)
end

context 'when the service provider is not on the allowlist for attempts api' do
it 'returns false' do
expect(service_provider.attempts_api_enabled?).to be(false)
end
end

context 'when the service provider is on the allowlist for attempts api' do
before do
allow(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return(
[{ 'issuer' => service_provider.issuer }],
)
end

it 'returns true' do
expect(service_provider.attempts_api_enabled?).to be(true)
end
end
end

context 'when attempts api availability is disabled' do
before do
allow(IdentityConfig.store).to receive(:attempts_api_enabled)
.and_return(false)
end

context 'when the service provider is on the allowlist for attempts api' do
before do
allow(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return(
[{ 'issuer' => service_provider.issuer }],
)
end

it 'returns false' do
expect(service_provider.attempts_api_enabled?).to be(false)
end
end

context 'when the service provider is not on the allowlist for attempts api' do
it 'returns false' do
expect(service_provider.attempts_api_enabled?).to be(false)
end
end
end
end

describe '#attempts_public_key' do
context 'when the sp is configured to use the attempts api' do
context 'when there is no public key set in the configuration' do
before do
allow(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return(
[{ 'issuer' => service_provider.issuer }],
)
end

it "returns the sp's first public key" do
expect(service_provider.attempts_public_key.to_pem).to eq(
service_provider.ssl_certs.first.public_key.to_pem,
)
end
end

context 'when the public key is set in the configuration' do
let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
let(:public_key) { private_key.public_key }

before do
allow(IdentityConfig.store).to receive(:allowed_attempts_providers).and_return(
[
{ 'issuer' => service_provider.issuer,
'keys' => [public_key.to_pem] },

],
)
end

it "returns the sp's first public key" do
expect(service_provider.attempts_public_key.to_pem).to eq(
public_key.to_pem,
)
end
end
end
end

describe '#ssl_certs' do
context 'with an empty string plural cert' do
let(:service_provider) { build(:service_provider, certs: ['']) }
Expand Down
2 changes: 0 additions & 2 deletions spec/services/attempts_api/tracker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
let(:cookie_device_uuid) { 'device_id' }
let(:sp_request_uri) { 'https://example.com/auth_page' }
let(:user) { create(:user) }
let(:analytics) { FakeAnalytics.new }

subject do
described_class.new(
Expand All @@ -30,7 +29,6 @@
cookie_device_uuid: cookie_device_uuid,
sp_request_uri: sp_request_uri,
enabled_for_session: enabled_for_session,
analytics: analytics,
)
end

Expand Down