Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6c5d13c
LG-8084 Hybrid Handoff SMS (#7499)
theabrad Dec 16, 2022
209f0ba
added elapsed_time to analytics irs_attempts_api_events (#7504)
Rwolfe-Nava Dec 16, 2022
b995562
LG-8407: PO Search: search button shows loading state (#7502)
allthesignals Dec 18, 2022
10cf687
Fix "No such file" error on "make normalize_yaml" (#7507)
aduth Dec 19, 2022
b326a97
Render ValidatedFieldComponent as aria-invalid when errors exist (#7503)
aduth Dec 19, 2022
33ea85e
Rename "Improvements" changelog category (#7511)
aduth Dec 19, 2022
875cdc1
Colocate GitHub issue templates (#7510)
aduth Dec 19, 2022
28ac3ed
Improve reliability of flakey countdown spec (#7512)
aduth Dec 19, 2022
d05fbcc
LG-7483: Remove feature flag select_multiple_mfa_options (#7505)
aduth Dec 20, 2022
0075355
LG-8373: Update PO Search "About" text to pilot copy (#7501)
jess-fortier Dec 20, 2022
de3d7d5
Initialize JavaScript tests with polyfilled fetch (#7518)
aduth Dec 20, 2022
a7577b6
LG-8432: SAML - Update ForceAuthn check (#7514)
julialeague Dec 20, 2022
76eae0b
LG-8089: Update to support notifying user of OTP format in message (#…
mdiarra3 Dec 20, 2022
ad58354
Revert "LG-8432: SAML - Update ForceAuthn check (#7514)" (#7520)
orenyk Dec 20, 2022
5e9a119
LG-8388: Confirm ThreatMetrix doesn't block IPP (#7521)
solipet Dec 21, 2022
b5d7f6b
Switching newer Acuant SDK to be the default version (#7513)
eric-gade Dec 21, 2022
510225d
Add logging criteria, abstract logging decision into method (#7516)
jskinne3 Dec 21, 2022
307f4dc
Add filename fingerprint to JavaScript locale data script (#7527)
aduth Dec 21, 2022
521fdd1
LG-8438: Update language on pages related to OTP (#7515)
jc-gsa Dec 21, 2022
9c009b4
Doc auth retry timer should decrease (#7498)
Dec 21, 2022
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
File renamed without changes.
File renamed without changes.
15 changes: 9 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ pull request is about.
- If the pull request is in response to a Jira ticket, include the ticket ID in
the commit title (e.g. "LG-1234 Add the stuff to the thing")

- Include a changelog message which describes the changes in human-readable
terms. These messages are included in release notes, so they should be easy to
understand for our partners and users. In the rare case that a change should
not be included in release notes, add `[skip changelog]` to the commit.
- Include a changelog message which describes the changes in human-readable terms. Refer to the
[_Changelog Messages_ section](#changelog-messages) below for specific changelog requirements.

Example:

Expand All @@ -52,7 +50,9 @@ changelog: Internal, Automated Testing, Improve performance of test suite

#### Changelog Messages

You must include a changelog message in one commit of your pull request.
You must include a changelog message in one commit of your pull request. The changelog message
describes the changes in human-readable terms. These messages are included in release notes, so they
should be easy to understand for our partners and users.

A changelog message should be written in the following format:

Expand All @@ -63,7 +63,7 @@ changelog: [Category], [Subcategory], [Description]
Replace `[Category]`, `[Subcategory]`, and `[Description]` with text relevant for your changes:

- **Category** must be one of the following:
- **Improvements** are user-facing improvements to the application experience, such as a new UI component or updated text.
- **User-Facing Improvements** are improvements to the application experience benefitting the end-user, such as a new UI component or updated text.
- **Bug Fixes** are corrections to a broken behavior, such as preventing a raised exception.
- **Internal** are changes which benefit the Login.gov team, such as analytics or code quality.
- **Upcoming Features** are iterations contributing to a feature which has not yet been enabled for users in production.
Expand All @@ -72,6 +72,9 @@ Replace `[Category]`, `[Subcategory]`, and `[Description]` with text relevant fo

If multiple pull requests iterate on the same feature, it's a good idea to use the same commit message, since identical messages will be combined into a single entry when the release notes are compiled.

In the rare case that a change should not be included in release notes, add `[skip changelog]` to
the commit.

### Additional notes on pull requests and code reviews

Please follow our [Code Review][review] guidelines.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ run-https: tmp/$(HOST)-$(PORT).key tmp/$(HOST)-$(PORT).crt ## Runs the developme

normalize_yaml: ## Normalizes YAML files (alphabetizes keys, fixes line length, smart quotes)
yarn normalize-yaml .rubocop.yml --disable-sort-keys --disable-smart-punctuation
find ./config/locales/telephony "./config/locales/telephony*" -type f | xargs yarn normalize-yaml --disable-smart-punctuation
find ./config/locales/telephony -type f | xargs yarn normalize-yaml --disable-smart-punctuation
find ./config/locales -not -path "./config/locales/telephony*" -type f | xargs yarn normalize-yaml \
config/pinpoint_supported_countries.yml \
config/pinpoint_overrides.yml \
Expand Down
2 changes: 1 addition & 1 deletion app/components/validated_field_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
input_html: {
class: [*tag_options.dig(:input_html, :class), 'validated-field__input'],
aria: {
invalid: false,
invalid: has_errors?,
describedby: aria_describedby_idrefs,
},
},
Expand Down
4 changes: 2 additions & 2 deletions app/components/validated_field_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ def aria_describedby_idrefs
idrefs
end

private

def has_errors?
form.object.respond_to?(:errors) && form.object.errors.key?(name)
end
Expand All @@ -44,6 +42,8 @@ def hint_id
"validated-field-hint-#{unique_id}"
end

private

def value_missing_error_message
case input_type
when :boolean
Expand Down
22 changes: 19 additions & 3 deletions app/controllers/api/irs_attempts_api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class IrsAttemptsApiController < ApplicationController
respond_to :json

def create
start_time = Time.zone.now.to_f
if timestamp
if IdentityConfig.store.irs_attempt_api_aws_s3_enabled
if IrsAttemptApiLogFile.find_by(requested_time: timestamp_key(key: timestamp))
Expand Down Expand Up @@ -53,7 +54,12 @@ def create
render json: { status: :unprocessable_entity, description: 'Invalid timestamp parameter' },
status: :unprocessable_entity
end
analytics.irs_attempts_api_events(**analytics_properties(authenticated: true))
analytics.irs_attempts_api_events(
**analytics_properties(
authenticated: true,
elapsed_time: elapsed_time(start_time),
),
)
end

private
Expand All @@ -62,7 +68,12 @@ def authenticate_client
bearer, csp_id, token = request.authorization&.split(' ', 3)
if bearer != 'Bearer' || !valid_auth_tokens.include?(token) ||
csp_id != IdentityConfig.store.irs_attempt_api_csp_id
analytics.irs_attempts_api_events(**analytics_properties(authenticated: false))
analytics.irs_attempts_api_events(
**analytics_properties(
authenticated: false,
elapsed_time: 0,
),
)
render json: { status: 401, description: 'Unauthorized' }, status: :unauthorized
end
end
Expand Down Expand Up @@ -99,10 +110,11 @@ def valid_auth_tokens
IdentityConfig.store.irs_attempt_api_auth_tokens
end

def analytics_properties(authenticated:)
def analytics_properties(authenticated:, elapsed_time:)
{
rendered_event_count: security_event_tokens.count,
timestamp: timestamp&.iso8601,
elapsed_time: elapsed_time,
authenticated: authenticated,
success: authenticated && timestamp.present?,
}
Expand All @@ -118,5 +130,9 @@ def timestamp
rescue ArgumentError
nil
end

def elapsed_time(start_time)
Time.zone.now.to_f - start_time
end
end
end
7 changes: 1 addition & 6 deletions app/controllers/concerns/mfa_setup_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module MfaSetupConcern
extend ActiveSupport::Concern

def next_setup_path
if user_needs_confirmation_screen?
if suggest_second_mfa?
auth_method_confirmation_url
elsif next_setup_choice
confirmation_path
Expand Down Expand Up @@ -45,11 +45,6 @@ def confirm_user_authenticated_for_2fa_setup
redirect_to user_two_factor_authentication_url
end

def user_needs_confirmation_screen?
suggest_second_mfa? &&
IdentityConfig.store.select_multiple_mfa_options
end

def in_multi_mfa_selection_flow?
return false unless user_session[:mfa_selections].present?
mfa_selection_index < mfa_selection_count
Expand Down
6 changes: 0 additions & 6 deletions app/controllers/users/mfa_selection_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ class MfaSelectionController < ApplicationController

before_action :authenticate_user
before_action :confirm_user_authenticated_for_2fa_setup
before_action :multiple_factors_enabled?

def index
two_factor_options_form
Expand Down Expand Up @@ -70,10 +69,5 @@ def process_valid_form
def two_factor_options_form_params
params.require(:two_factor_options_form).permit(:selection, selection: [])
end

def multiple_factors_enabled?
return if IdentityConfig.store.select_multiple_mfa_options
redirect_to after_mfa_setup_path
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def send_user_otp(method)
to: phone_to_deliver_to,
otp: current_user.direct_otp,
expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES,
otp_format: t('telephony.format_type.digit'),
channel: method.to_sym,
domain: IdentityConfig.store.domain_name,
country_code: parsed_phone.country,
Expand Down
11 changes: 6 additions & 5 deletions app/forms/two_factor_options_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TwoFactorOptionsForm
backup_code] }

validates :selection, length: { minimum: 1 }, if: :has_no_mfa_or_in_required_flow?
validates :selection, length: { minimum: 2, message: 'phone' }, if: :phone_validations?
validates :selection, length: { minimum: 2, message: 'phone' }, if: :phone_valid?

def initialize(user:, phishing_resistant_required:, piv_cac_required:)
self.user = user
Expand Down Expand Up @@ -74,9 +74,10 @@ def phone_alternative_enabled?
count >= 2 || (count == 1 && MfaContext.new(user).phone_configurations.none?)
end

def phone_validations?
IdentityConfig.store.select_multiple_mfa_options &&
phone_selected? && has_no_configured_mfa? &&
!phone_alternative_enabled? && kantara_2fa_phone_restricted?
def phone_valid?
phone_selected? &&
has_no_configured_mfa? &&
!phone_alternative_enabled? &&
kantara_2fa_phone_restricted?
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { render } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import type { SetupServerApi } from 'msw/node';
import { useSandbox } from '@18f/identity-test-helpers';
import userEvent from '@testing-library/user-event';
import AddressSearch, { ADDRESS_SEARCH_URL } from './address-search';

const DEFAULT_RESPONSE = [
{
address: '100 Main St E, Bronwood, Georgia, 39826',
location: {
latitude: 31.831686000000005,
longitude: -84.363768,
},
street_address: '100 Main St E',
city: 'Bronwood',
state: 'GA',
zip_code: '39826',
},
];

describe('AddressSearch', () => {
const sandbox = useSandbox();

let server: SetupServerApi;
before(() => {
server = setupServer(
rest.post(ADDRESS_SEARCH_URL, (_req, res, ctx) => res(ctx.json(DEFAULT_RESPONSE))),
);
server.listen();
});

after(() => {
server.close();
});

it('fires the callback with correct input', async () => {
const handleAddressFound = sandbox.stub();
const { findByText, findByLabelText } = render(
<AddressSearch onAddressFound={handleAddressFound} />,
);

await userEvent.type(
await findByLabelText('in_person_proofing.body.location.po_search.address_search_label'),
'200 main',
);
await userEvent.click(
await findByText('in_person_proofing.body.location.po_search.search_button'),
);

await expect(handleAddressFound).to.eventually.be.calledWith(DEFAULT_RESPONSE[0]);
});

it('validates input and shows inline error', async () => {
const { findByText } = render(<AddressSearch />);

await userEvent.click(
await findByText('in_person_proofing.body.location.po_search.search_button'),
);

await findByText('in_person_proofing.body.location.inline_error');
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { TextInput, Button } from '@18f/identity-components';
import { TextInput } from '@18f/identity-components';
import { request } from '@18f/identity-request';
import { useState, useCallback, ChangeEvent, useRef, Ref } from 'react';
import { useState, useCallback, ChangeEvent, useRef, useEffect } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import ValidatedField from '@18f/identity-validated-field/validated-field';
import SpinnerButton, { SpinnerButtonRefHandle } from '@18f/identity-spinner-button/spinner-button';
import type { RegisterFieldCallback } from '@18f/identity-form-steps';
import useSWR from 'swr';

interface Location {
street_address: string;
Expand All @@ -14,31 +17,49 @@ interface Location {

interface AddressSearchProps {
onAddressFound?: (location: Location) => void;
registerField: (field: string) => Ref<HTMLInputElement>;
registerField?: RegisterFieldCallback;
}

export const ADDRESS_SEARCH_URL = '/api/addresses';

function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSearchProps) {
function requestAddressCandidates(unvalidatedAddressInput: string): Promise<Location[]> {
return request<Location[]>(ADDRESS_SEARCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
json: { address: unvalidatedAddressInput },
});
}

function AddressSearch({
onAddressFound = () => {},
registerField = () => undefined,
}: AddressSearchProps) {
const validatedFieldRef = useRef<HTMLFormElement | null>(null);
const [unvalidatedAddressInput, setUnvalidatedAddressInput] = useState('');
const [addressQuery, setAddressQuery] = useState('');
const { t } = useI18n();
const { data: addressCandidates } = useSWR([ADDRESS_SEARCH_URL, addressQuery], () =>
addressQuery ? requestAddressCandidates(unvalidatedAddressInput) : null,
);
const ref = useRef<SpinnerButtonRefHandle>(null);

useEffect(() => {
if (addressCandidates) {
const bestMatchedAddress = addressCandidates[0];
onAddressFound(bestMatchedAddress);
ref.current?.toggleSpinner(false);
}
}, [addressCandidates]);

const handleAddressSearch = useCallback(
async (event) => {
(event) => {
event.preventDefault();
validatedFieldRef.current?.reportValidity();
if (unvalidatedAddressInput === '') {
return;
}
const addressCandidates = await request<Location>(ADDRESS_SEARCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
json: { address: unvalidatedAddressInput },
});

const bestMatchedAddress = addressCandidates[0];
onAddressFound(bestMatchedAddress);
setAddressQuery(unvalidatedAddressInput);
},
[unvalidatedAddressInput],
);
Expand All @@ -63,9 +84,16 @@ function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSear
hint={t('in_person_proofing.body.location.po_search.address_search_hint')}
/>
</ValidatedField>
<Button type="submit" className="margin-y-5" onClick={handleAddressSearch}>
<SpinnerButton
isWide
isBig
ref={ref}
type="submit"
className="margin-y-5"
onClick={handleAddressSearch}
>
{t('in_person_proofing.body.location.po_search.search_button')}
</Button>
</SpinnerButton>
</>
);
}
Expand Down
Loading