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
63 changes: 59 additions & 4 deletions app/javascript/packages/webauthn/enroll-webauthn-device.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ describe('enrollWebauthnDevice', () => {
225, 190, 13, 223, 243, 75, 174, 252, 212, 215, 183, 9,
]).buffer;

beforeEach(() => {
function defineNavigatorCredentials({
getAuthenticatorData,
getTransports,
}: {
getAuthenticatorData?: AuthenticatorAttestationResponse['getAuthenticatorData'];
getTransports?: AuthenticatorAttestationResponse['getTransports'];
}) {
defineProperty(navigator, 'credentials', {
configurable: true,
value: {
Expand All @@ -34,12 +40,20 @@ describe('enrollWebauthnDevice', () => {
response: {
attestationObject: Buffer.from('attest', 'utf-8'),
clientDataJSON: Buffer.from('json', 'utf-8'),
getAuthenticatorData: () => authenticatorData,
getTransports: () => ['usb'],
getAuthenticatorData,
getTransports,
},
}),
},
});
}

context('fully supported AuthenticatorAttestationResponse', () => {
beforeEach(() => {
defineNavigatorCredentials({
getAuthenticatorData: () => authenticatorData,
getTransports: () => ['usb'],
});
});

it('enrolls a device using the proper create options', async () => {
Expand Down Expand Up @@ -92,7 +106,7 @@ describe('enrollWebauthnDevice', () => {
webauthnPublicKey: '123',
attestationObject: btoa('attest'),
clientDataJSON: btoa('json'),
authenticatorDataValue: 65,
authenticatorDataFlagsValue: 65,
transports: ['usb'],
});
});
Expand Down Expand Up @@ -131,3 +145,44 @@ describe('enrollWebauthnDevice', () => {
});
});
});

context('AuthenticatorAttestationResponse#getTransports unsupported', () => {
beforeEach(() => {
defineNavigatorCredentials({
getAuthenticatorData: () => authenticatorData,
getTransports: undefined,
});
});

it('enrolls a device with a blank transports result', async () => {
const result = await enrollWebauthnDevice({
user,
challenge,
excludeCredentials,
authenticatorAttachment: 'cross-platform',
});

expect(result.transports).to.equal(undefined);
});
});

context('AuthenticatorAttestationResponse#getAuthenticatorData unsupported', () => {
beforeEach(() => {
defineNavigatorCredentials({
getAuthenticatorData: undefined,
getTransports: () => ['usb'],
});
});

it('enrolls a device with a blank authenticatorDataFlagsValue result', async () => {
const result = await enrollWebauthnDevice({
user,
challenge,
excludeCredentials,
authenticatorAttachment: 'cross-platform',
});

expect(result.authenticatorDataFlagsValue).to.equal(undefined);
});
});
});
31 changes: 25 additions & 6 deletions app/javascript/packages/webauthn/enroll-webauthn-device.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { arrayBufferToBase64 } from './converters';

/**
* Response object with properties as possibly undefined where browser support varies.
*
* As of writing, Firefox does not implement getTransports or getAuthenticatorData. Remove this if
* and when support changes.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/getTransports#browser_compatibility
* @see https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/getAuthenticatorData#browser_compatibility
*/
interface AuthenticatorAttestationResponseBrowserSupport
extends Omit<AuthenticatorAttestationResponse, 'getAuthenticatorData' | 'getTransports'> {
getTransports: AuthenticatorAttestationResponse['getTransports'] | undefined;
getAuthenticatorData: AuthenticatorAttestationResponse['getAuthenticatorData'] | undefined;
}

interface EnrollOptions {
user: PublicKeyCredentialUserEntity;

Expand All @@ -19,9 +34,9 @@ interface EnrollResult {

clientDataJSON: string;

authenticatorDataValue: number;
authenticatorDataFlagsValue?: number;

transports: string[];
transports?: string[];
}

async function enrollWebauthnDevice({
Expand Down Expand Up @@ -76,15 +91,19 @@ async function enrollWebauthnDevice({
},
})) as PublicKeyCredential;

const response = credential.response as AuthenticatorAttestationResponse;
const authenticatorDataValue = new Uint8Array(response.getAuthenticatorData())[32];
const response = credential.response as AuthenticatorAttestationResponseBrowserSupport;
const authenticatorData = response.getAuthenticatorData?.();
const authenticatorDataFlagsValue = authenticatorData
? new Uint8Array(authenticatorData)[32]
: undefined;

return {
webauthnId: arrayBufferToBase64(credential.rawId),
webauthnPublicKey: credential.id,
attestationObject: arrayBufferToBase64(response.attestationObject),
clientDataJSON: arrayBufferToBase64(response.clientDataJSON),
authenticatorDataValue,
transports: response.getTransports(),
authenticatorDataFlagsValue,
transports: response.getTransports?.(),
};
}

Expand Down
6 changes: 5 additions & 1 deletion app/javascript/packs/webauthn-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,15 @@ function webauthn() {
result.attestationObject;
(document.getElementById('client_data_json') as HTMLInputElement).value =
result.clientDataJSON;
if (result.authenticatorDataFlagsValue) {
(
document.getElementById('authenticator_data_value') as HTMLInputElement
).value = `${result.authenticatorDataValue}`;
).value = `${result.authenticatorDataFlagsValue}`;
}
if (result.transports) {
(document.getElementById('transports') as HTMLInputElement).value =
result.transports.join();
}
Comment thread
aduth marked this conversation as resolved.
(document.getElementById('webauthn_form') as HTMLFormElement).submit();
})
.catch((error: Error) => {
Expand Down
51 changes: 14 additions & 37 deletions spec/forms/webauthn_setup_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,62 +77,39 @@
end

context 'with non backed up option data flags' do
let(:params) do
{
attestation_object: attestation_object,
client_data_json: setup_client_data_json,
name: 'mykey',
platform_authenticator: false,
transports: 'usb',
authenticator_data_value: '65',
}
end
let(:params) { super().merge(authenticator_data_value: '65') }

it 'includes data flags with bs set as false ' do
result = subject.submit(protocol, params)

expect(result.to_h).to eq(
success: true,
errors: {},
enabled_mfa_methods_count: 1,
mfa_method_counts: { webauthn: 1 },
multi_factor_auth_method: 'webauthn',
authenticator_data_flags: {
expect(result.to_h[:authenticator_data_flags]).to eq(
up: true,
uv: false,
be: false,
bs: false,
at: true,
ed: false,
},
pii_like_keypaths: [[:mfa_method_counts, :phone]],
)
end
end

context 'when user enters in a non number value' do
let(:params) do
{
attestation_object: attestation_object,
client_data_json: setup_client_data_json,
name: 'mykey',
platform_authenticator: false,
transports: 'usb',
authenticator_data_value: 'bad_error',
}
context 'when authenticator_data_value is not a number' do
let(:params) { super().merge(authenticator_data_value: 'bad_error') }

it 'should not include authenticator data flag' do
result = subject.submit(protocol, params)

expect(result.to_h[:authenticator_data_flags]).to be_nil
end
end

context 'when authenticator_data_value is missing' do
let(:params) { super().merge(authenticator_data_value: nil) }

it 'should not include authenticator data flag' do
result = subject.submit(protocol, params)

expect(result.to_h).to eq(
success: true,
errors: {},
enabled_mfa_methods_count: 1,
mfa_method_counts: { webauthn: 1 },
multi_factor_auth_method: 'webauthn',
pii_like_keypaths: [[:mfa_method_counts, :phone]],
)
expect(result.to_h[:authenticator_data_flags]).to be_nil
end
end
end
Expand Down