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
3 changes: 1 addition & 2 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ checks:
config:
threshold: 4
complex-logic:
config:
threshold: 4
enabled: false
file-lines:
enabled: false
method-complexity:
Expand Down
6 changes: 3 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ gem 'rails', '~> 6.1.4'
@hostdata_gem ||= { github: '18F/identity-hostdata', tag: 'v3.4.0' }
@logging_gem ||= { github: '18F/identity-logging', tag: 'v0.1.0' }
@saml_gem ||= { github: '18F/saml_idp', tag: 'v0.14.3-18f' }
@telephony_gem ||= { github: '18f/identity-telephony', tag: 'v0.4.4' }
@validations_gem ||= { github: '18F/identity-validations', tag: 'v0.7.1' }

gem 'identity-hostdata', @hostdata_gem
gem 'identity-logging', @logging_gem
gem 'identity-telephony', @telephony_gem
gem 'identity_validations', @validations_gem
gem 'saml_idp', @saml_gem

gem 'ahoy_matey', '~> 3.0'
gem 'autoprefixer-rails', '~> 10.0'
gem 'aws-sdk-kms', '~> 1.4'
gem 'aws-sdk-ses', '~> 1.6'
gem 'aws-sdk-pinpoint'
gem 'aws-sdk-pinpointsmsvoice'
gem 'base32-crockford'
gem 'bootsnap', '~> 1.9.0', require: false
gem 'blueprinter', '~> 0.25.3'
Expand Down Expand Up @@ -53,7 +53,7 @@ gem 'rack-timeout', require: false
gem 'redacted_struct'
gem 'redis', '>= 3.2.0'
gem 'redis-namespace'
gem 'redis-session-store', '>= 0.11.3'
gem 'redis-session-store', github: '18f/redis-session-store', tag: 'v0.11.4-18f'
gem 'retries'
gem 'rotp', '~> 6.1'
gem 'rqrcode'
Expand Down
1 change: 0 additions & 1 deletion Gemfile-dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ FileUtils.cp("Gemfile.lock", "Gemfile-dev.lock")
# @hostdata_gem = { path: '../identity-hostdata' }
# @idp_functions_gem = { path: '../identity-idp-functions' }
# @logging_gem = { path: '../identity-logging' }
# @telephony_gem = { path: '../identity-telephony' }
# @validations_gem = { path: '../identity-validations' }
# @saml_gem = { path: '../saml_idp' }

Expand Down
21 changes: 9 additions & 12 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ GIT
uuid

GIT
remote: https://github.com/18f/identity-telephony.git
revision: 15e9eb147900e959e130de1bf409ee1814e24ecc
tag: v0.4.4
remote: https://github.com/18f/redis-session-store.git
revision: d3f5e38d3a6173737a580d774767a0d8471b1e67
tag: v0.11.4-18f
specs:
identity-telephony (0.4.4)
aws-sdk-pinpoint
aws-sdk-pinpointsmsvoice
i18n
redis-session-store (0.11.4.pre.18f)
actionpack (>= 3, < 7)
redis (>= 3, < 5)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -485,9 +484,6 @@ GEM
redis (4.4.0)
redis-namespace (1.8.1)
redis (>= 3.0.4)
redis-session-store (0.11.3)
actionpack (>= 3, < 7)
redis (>= 3, < 5)
regexp_parser (2.1.1)
reline (0.2.5)
io-console (~> 0.5)
Expand Down Expand Up @@ -690,6 +686,8 @@ DEPENDENCIES
autoprefixer-rails (~> 10.0)
aws-sdk-cloudwatchlogs
aws-sdk-kms (~> 1.4)
aws-sdk-pinpoint
aws-sdk-pinpointsmsvoice
aws-sdk-ses (~> 1.6)
axe-core-rspec (~> 4.2)
base32-crockford
Expand Down Expand Up @@ -721,7 +719,6 @@ DEPENDENCIES
i18n-tasks (>= 0.9.31)
identity-hostdata!
identity-logging!
identity-telephony!
identity_validations!
irb
jwt
Expand Down Expand Up @@ -758,7 +755,7 @@ DEPENDENCIES
redacted_struct
redis (>= 3.2.0)
redis-namespace
redis-session-store (>= 0.11.3)
redis-session-store!
retries
rotp (~> 6.1)
rqrcode
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/concerns/idv/document_capture_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ def override_document_capture_step_csp
request,
# required to run wasm until wasm-eval is available
script_src: ['\'unsafe-eval\''],
# required because acuant styles its own elements with inline style attributes
style_src: ['\'unsafe-inline\''],
# required for retrieving image dimensions from uploaded images
img_src: ['blob:'],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class AuthorizationConfirmationController < ApplicationController
def show
analytics.track_event(Analytics::AUTHENTICATION_CONFIRMATION)
@sp = ServiceProvider.find_by(issuer: sp_session[:issuer])
@email = EmailContext.new(current_user).last_sign_in_email_address.email
end

def update
Expand Down
5 changes: 4 additions & 1 deletion app/forms/webauthn_setup_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ def valid_attestation_response?(protocol)
def safe_response(original_origin)
@attestation_response.valid?(@challenge.pack('c*'), original_origin)
rescue StandardError
errors.add :name, I18n.t('errors.webauthn_setup.attestation_error')
errors.add :name, I18n.t(
'errors.webauthn_setup.attestation_error',
link: MarketingSite.contact_url,
)
false
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { useContext, useMemo, useEffect, useRef, useState } from 'react';
import { useContext, useEffect, useRef, useState } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import AcuantContext from '../context/acuant';
import useAsset from '../hooks/use-asset';
import useInstanceId from '../hooks/use-instance-id';
import useImmutableCallback from '../hooks/use-immutable-callback';
import './acuant-capture-canvas.scss';

/**
* @enum {string}
*/
const CaptureStatus = {
ALIGN: 'ALIGN',
MOVE_CLOSER: 'MOVE_CLOSER',
TAP_TO_CAPTURE: 'TAP_TO_CAPTURE',
CAPTURING: 'CAPTURING',
};
/** @typedef {import('../context/acuant').AcuantJavaScriptWebSDK} AcuantJavaScriptWebSDK */

/**
* @enum {number}
Expand Down Expand Up @@ -92,13 +84,14 @@ export function defineObservableProperty(object, property, onChangeCallback) {

/**
* @typedef {(
* | null // Cropping failure (SDK v11.4.3, L753)
* | undefined // Cropping failure (SDK v11.4.3, L960)
* | 'Camera not supported.' // Camera not supported (SDK v11.4.3, L74, L798)
* | 'already started.' // Capture already started (SDK v11.4.3, L565)
* | 'already started' // Capture already started (SDK v11.4.3, L580)
* | 'Missing HTML elements.' // Required page elements are not available (SDK v11.4.3, L568)
* | MediaStreamError // User or system denied camera access (SDK v11.4.3, L544)
* | undefined // Cropping failure (SDK v11.5.0, L1171)
* | 'Camera not supported.' // Camera not supported (SDK v11.5.0, L978)
* | 'already started.' // Capture already started (SDK v11.5.0, L724)
* | 'Missing HTML elements.' // Required page elements are not available (SDK v11.5.0, L727)
* | Error // User or system denied camera access (SDK v11.5.0, L673)
* | "Expected div with 'acuant-camera' id" // Failure to setup due to missing element (SDK v11.5.0, L706)
* | 'Live capture has previously failed and was called again. User was sent to manual capture.' // Previous failure (SDK v11.5.0, L698)
* | 'sequence-break' // iOS 15 sequence break (SDK v11.5.0, L1327)
* )} AcuantCaptureFailureError
*/

Expand Down Expand Up @@ -127,6 +120,7 @@ export function defineObservableProperty(object, property, onChangeCallback) {
* @typedef AcuantCamera
*
* @prop {(callback: (response: AcuantImage)=>void, errorCallback: function)=>void} start
* @prop {(callback: AcuantCameraUICallbacks)=>void} startManualCapture
* @prop {(callback: (response: AcuantImage)=>void)=>void} triggerCapture
* @prop {(
* data: string,
Expand All @@ -142,6 +136,7 @@ export function defineObservableProperty(object, property, onChangeCallback) {
*
* @prop {AcuantCameraUI} AcuantCameraUI Acuant camera UI API.
* @prop {AcuantCamera} AcuantCamera Acuant camera API.
* @prop {AcuantJavaScriptWebSDK} AcuantJavascriptWebSdk Acuant web SDK.
*/

/**
Expand Down Expand Up @@ -189,7 +184,7 @@ export function defineObservableProperty(object, property, onChangeCallback) {
*/

/**
* @typedef {(error:AcuantCaptureFailureError)=>void} AcuantFailureCallback
* @typedef {(error?: AcuantCaptureFailureError, code?: string) => void} AcuantFailureCallback
*/

/**
Expand All @@ -199,55 +194,6 @@ export function defineObservableProperty(object, property, onChangeCallback) {
* @prop {AcuantFailureCallback} onImageCaptureFailure Failure callback.
*/

/**
* Returns the computed capture status based on current capture type and frame state.
*
* @param {AcuantCaptureType} captureType Current capture type.
* @param {AcuantFrameState} frameState Current frame state.
*
* @return {CaptureStatus}
*/
function getCaptureStatus(captureType, frameState) {
// Acuant internally updates UI state to "Tap to Capture" but does _not_ invoke the
// `onFrameAvailable` callback, so we have to track `captureType` separately.
if (captureType === 'TAP' || frameState === AcuantUIState.TAP_TO_CAPTURE) {
return CaptureStatus.TAP_TO_CAPTURE;
}

switch (frameState) {
case AcuantDocumentState.GOOD_DOCUMENT:
return CaptureStatus.CAPTURING;
case AcuantDocumentState.SMALL_DOCUMENT:
return CaptureStatus.MOVE_CLOSER;
default:
return CaptureStatus.ALIGN;
}
}

/**
* Returns the translation key to use for the status text based on current capture status.
*
* @param {CaptureStatus} captureStatus
*
* @return {string}
*/
function getStatusLabelKey(captureStatus) {
switch (captureStatus) {
case CaptureStatus.CAPTURING:
// i18n-tasks-use t('doc_auth.accessible_labels.status_capturing')
return 'doc_auth.accessible_labels.status_capturing';
case CaptureStatus.MOVE_CLOSER:
// i18n-tasks-use t('doc_auth.accessible_labels.status_move_closer')
return 'doc_auth.accessible_labels.status_move_closer';
case CaptureStatus.TAP_TO_CAPTURE:
// i18n-tasks-use t('doc_auth.accessible_labels.status_tap_to_capture')
return 'doc_auth.accessible_labels.status_tap_to_capture';
default:
// i18n-tasks-use t('doc_auth.accessible_labels.status_align')
return 'doc_auth.accessible_labels.status_align';
}
}

/**
* @param {AcuantCaptureCanvasProps} props Component props.
*/
Expand All @@ -258,49 +204,40 @@ function AcuantCaptureCanvas({
const { isReady } = useContext(AcuantContext);
const { getAssetPath } = useAsset();
const { t } = useI18n();
const instanceId = useInstanceId();
const [hasCaptured, setHasCaptured] = useState(false);
const canvasRef = useRef(/** @type {(HTMLCanvasElement & {callback: function})?} */ (null));
const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null));
const onCropped = useImmutableCallback(
(response) => {
if (response) {
onImageCaptureSuccess(response);
} else {
onImageCaptureFailure(response);
onImageCaptureFailure();
}
},
[onImageCaptureSuccess, onImageCaptureFailure],
[onImageCaptureSuccess],
);
const [captureType, setCaptureType] = useState(/** @type {AcuantCaptureType} */ ('AUTO'));
const [frameState, setFrameState] = useState(
/** @type {AcuantFrameState} */ (AcuantDocumentState.NO_DOCUMENT),
);
const captureStatus = useMemo(() => getCaptureStatus(captureType, frameState), [
captureType,
frameState,
]);

useEffect(() => {
if (canvasRef.current) {
function onAcuantCameraCreated() {
const canvas = document.getElementById('acuant-ui-canvas');
// Acuant SDK assigns a callback property to the canvas when it switches to its "Tap to
// Capture" mode (Acuant SDK v11.4.4, L158). Infer capture type by presence of the property.
defineObservableProperty(canvasRef.current, 'callback', (callback) => {
defineObservableProperty(canvas, 'callback', (callback) => {
setCaptureType(callback ? 'TAP' : 'AUTO');
});
}

cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated);
return () => {
cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated);
};
}, []);

useEffect(() => {
if (isReady) {
setHasCaptured(false);
/** @type {AcuantGlobal} */ (window).AcuantCameraUI.start(
{
onFrameAvailable(result) {
setFrameState(result.state);
},
onCaptured() {
setHasCaptured(true);
},
onCaptured() {},
onCropped,
},
onImageCaptureFailure,
Expand All @@ -323,8 +260,7 @@ function AcuantCaptureCanvas({
};
}, [isReady]);

// The video element is never visible to the user, but it needs to be present
// in the DOM for the Acuant SDK to capture the feed from the camera.
const clickCanvas = () => document.getElementById('acuant-ui-canvas')?.click();

return (
<>
Expand All @@ -338,47 +274,24 @@ function AcuantCaptureCanvas({
alt=""
width="144"
height="144"
style={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-72px, -72px)',
}}
className="acuant-capture-canvas__spinner"
/>
)}
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video id="acuant-player" controls autoPlay playsInline style={{ display: 'none' }} />
<div id="acuant-sdk-capture-view">
<canvas
id="acuant-video-canvas"
ref={canvasRef}
tabIndex={0}
aria-labelledby={`acuant-sdk-heading-${instanceId}`}
aria-describedby={`acuant-sdk-instructions-${instanceId}`}
style={{
width: '100%',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<h2 key="label" id={`acuant-sdk-heading-${instanceId}`}>
{t('doc_auth.accessible_labels.camera_video_capture_label')}
</h2>
{captureType !== 'TAP' && (
<p key="description" id={`acuant-sdk-instructions-${instanceId}`}>
{t('doc_auth.accessible_labels.camera_video_capture_instructions')}
</p>
)}
<button key="button" type="button" disabled={captureType !== 'TAP'}>
{t('doc_auth.buttons.take_picture')}
</button>
</canvas>
<div role="status" aria-live="polite" className="usa-sr-only">
{isReady && !hasCaptured ? t(getStatusLabelKey(captureStatus)) : null}
</div>
</div>
<h2 className="usa-sr-only">{t('doc_auth.accessible_labels.camera_video_capture_label')}</h2>
{captureType !== 'TAP' && (
<p className="usa-sr-only">
{t('doc_auth.accessible_labels.camera_video_capture_instructions')}
</p>
)}
<div id="acuant-camera" ref={cameraRef} className="acuant-capture-canvas__camera" />
<button
type="button"
onClick={clickCanvas}
disabled={captureType !== 'TAP'}
className="usa-sr-only"
>
{t('doc_auth.buttons.take_picture')}
</button>
</>
);
}
Expand Down
Loading