Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass through AuthenticatorAttestationResponse.getTransports() #44

Merged
merged 6 commits into from
Jan 11, 2022
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
9 changes: 2 additions & 7 deletions src/basic/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
CredentialRequestOptionsJSON,
PublicKeyCredentialWithAssertionJSON,
PublicKeyCredentialWithAttestationJSON,
PublicKeyCredentialWithClientExtensionResults,
} from "./json";
import {
credentialCreationOptions,
Expand All @@ -23,12 +22,10 @@ export function createRequestFromJSON(
export function createResponseToJSON(
credential: PublicKeyCredential,
): PublicKeyCredentialWithAttestationJSON {
const credentialWithClientExtensionResults = credential as PublicKeyCredentialWithClientExtensionResults;
credentialWithClientExtensionResults.clientExtensionResults = credential.getClientExtensionResults();
return convert(
bufferToBase64url,
publicKeyCredentialWithAttestation,
credentialWithClientExtensionResults,
credential,
);
}

Expand All @@ -50,12 +47,10 @@ export function getRequestFromJSON(
export function getResponseToJSON(
credential: PublicKeyCredential,
): PublicKeyCredentialWithAssertionJSON {
const credentialWithClientExtensionResults = credential as PublicKeyCredentialWithClientExtensionResults;
credentialWithClientExtensionResults.clientExtensionResults = credential.getClientExtensionResults();
return convert(
bufferToBase64url,
publicKeyCredentialWithAssertion,
credentialWithClientExtensionResults,
credential,
);
}

Expand Down
1 change: 1 addition & 0 deletions src/basic/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface CredentialCreationOptionsJSON {
export interface AuthenticatorAttestationResponseJSON {
clientDataJSON: Base64urlString;
attestationObject: Base64urlString;
transports: string[];
}

export interface PublicKeyCredentialWithAttestationJSON {
Expand Down
15 changes: 13 additions & 2 deletions src/basic/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Schema } from "../schema-format";
import {
convertValue as convert,
copyValue as copy,
derived,
optional,
required,
} from "../convert";
Expand Down Expand Up @@ -58,8 +59,15 @@ export const publicKeyCredentialWithAttestation: Schema = {
response: required({
clientDataJSON: required(convert),
attestationObject: required(convert),
transports: derived(
copy,
(response: any) => response.getTransports?.() || [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review note: I was a little concerned that empty transports field might mess with the authentication prompt, but it doesn't seem to be an issue. I'm having trouble finding a place to back that up in the spec, though, so I'm just going off of Chrome/FirefoxSafari on macOS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backing for this in the spec is in the definition of the getTransports() method: 🙂

These values are the transports that the authenticator is believed to support, or an empty sequence if the information is unavailable.

If the getTransports method doesn't exist, then the information is indeed not available, so I think this should be an appropriate fallback value.

See also the definition of navigator.credentials.get() - any credential descriptor with an empty transports member will simply not affect the distinctTransports variable being computed there.

),
}),
clientExtensionResults: required(simplifiedClientExtensionResultsSchema),
clientExtensionResults: derived(
simplifiedClientExtensionResultsSchema,
(pkc: PublicKeyCredential) => pkc.getClientExtensionResults(),
),
};

// `navigator.get()` request
Expand Down Expand Up @@ -89,7 +97,10 @@ export const publicKeyCredentialWithAssertion: Schema = {
signature: required(convert),
userHandle: required(convert),
}),
clientExtensionResults: required(simplifiedClientExtensionResultsSchema),
clientExtensionResults: derived(
simplifiedClientExtensionResultsSchema,
(pkc: PublicKeyCredential) => pkc.getClientExtensionResults(),
),
};

export const schema: { [s: string]: Schema } = {
Expand Down
24 changes: 21 additions & 3 deletions src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// We export these values in order so that they can be used to deduplicate
// schema definitions in minified JS code.

import { Schema } from "./schema-format";
import { Schema, SchemaProperty } from "./schema-format";

// TODO: Parcel isn't deduplicating these values.
export const copyValue = "copy";
Expand All @@ -24,6 +24,13 @@ export function convert<From, To>(
if (schema instanceof Object) {
const output: any = {};
for (const [key, schemaField] of Object.entries(schema)) {
if (schemaField.deriveFn) {
const v = schemaField.deriveFn(input);
if (v !== undefined) {
input[key] = v;
}
}

if (!(key in input)) {
if (schemaField.required) {
throw new Error(`Missing key: ${key}`);
Expand All @@ -47,14 +54,25 @@ export function convert<From, To>(
}
}

export function required(schema: Schema): any {
export function derived(
schema: Schema,
deriveFn: (v: any) => any,
): SchemaProperty {
return {
required: true,
schema,
deriveFn,
};
}

export function required(schema: Schema): SchemaProperty {
return {
required: true,
schema,
};
}

export function optional(schema: Schema): any {
export function optional(schema: Schema): SchemaProperty {
return {
required: false,
schema,
Expand Down
13 changes: 3 additions & 10 deletions src/extended/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import {
Base64urlString,
} from "../base64url";
import { convert } from "../convert";
import {
PublicKeyCredentialWithClientExtensionResults,
AuthenticatorAttestationResponseJSON,
} from "../basic/json";
import { AuthenticatorAttestationResponseJSON } from "../basic/json";
import {
CredentialCreationOptionsExtendedJSON,
CredentialRequestOptionsExtendedJSON,
Expand Down Expand Up @@ -38,12 +35,10 @@ export function createExtendedRequestFromJSON(
export function createExtendedResponseToJSON(
credential: PublicKeyCredential,
): PublicKeyCredentialWithAttestationExtendedResultsJSON {
const credentialWithClientExtensionResults = credential as PublicKeyCredentialWithClientExtensionResults;
credentialWithClientExtensionResults.clientExtensionResults = credential.getClientExtensionResults();
return convert(
bufferToBase64url,
publicKeyCredentialWithAttestationExtended,
credentialWithClientExtensionResults,
credential,
);
}

Expand Down Expand Up @@ -119,12 +114,10 @@ export function getExtendedRequestFromJSON(
export function getExtendedResponseToJSON(
credential: PublicKeyCredential,
): PublicKeyCredentialWithAssertionExtendedResultsJSON {
const credentialWithClientExtensionResults = credential as PublicKeyCredentialWithClientExtensionResults;
credentialWithClientExtensionResults.clientExtensionResults = credential.getClientExtensionResults();
return convert(
bufferToBase64url,
publicKeyCredentialWithAssertionExtended,
credentialWithClientExtensionResults,
credential,
);
}

Expand Down
9 changes: 6 additions & 3 deletions src/extended/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
publicKeyCredentialWithAssertion,
publicKeyCredentialWithAttestation,
} from "../basic/schema";
import { convertValue, copyValue, optional, required } from "../convert";
import { convertValue, copyValue, derived, optional } from "../convert";
import { Schema } from "../schema-format";

// shared
Expand Down Expand Up @@ -45,9 +45,11 @@ export const credentialCreationOptionsExtended: Schema = JSON.parse(
export const publicKeyCredentialWithAttestationExtended: Schema = JSON.parse(
JSON.stringify(publicKeyCredentialWithAttestation),
);
(publicKeyCredentialWithAttestationExtended as any).clientExtensionResults = required(
(publicKeyCredentialWithAttestationExtended as any).clientExtensionResults = derived(
authenticationExtensionsClientOutputsSchema,
(publicKeyCredentialWithAttestation as any).clientExtensionResults.deriveFn,
);
(publicKeyCredentialWithAttestationExtended as any).response.schema.transports = (publicKeyCredentialWithAttestation as any).response.schema.transports;
// get

export const credentialRequestOptionsExtended: Schema = JSON.parse(
Expand All @@ -60,6 +62,7 @@ export const credentialRequestOptionsExtended: Schema = JSON.parse(
export const publicKeyCredentialWithAssertionExtended: Schema = JSON.parse(
JSON.stringify(publicKeyCredentialWithAssertion),
);
(publicKeyCredentialWithAssertionExtended as any).clientExtensionResults = required(
(publicKeyCredentialWithAssertionExtended as any).clientExtensionResults = derived(
authenticationExtensionsClientOutputsSchema,
(publicKeyCredentialWithAssertion as any).clientExtensionResults.deriveFn,
);
7 changes: 6 additions & 1 deletion src/schema-format.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
type SchemaLeaf = "copy" | "convert";
export interface SchemaProperty {
required: boolean;
schema: Schema;
deriveFn?(v: any): any;
}
interface SchemaObject {
[property: string]: { required: boolean; schema: Schema };
[property: string]: SchemaProperty;
}
type SchemaArray = [SchemaObject] | [SchemaLeaf];

Expand Down
83 changes: 83 additions & 0 deletions test/extended.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { base64urlToBuffer } from "../src/base64url";
import { CredentialCreationOptionsExtendedJSON } from "../src/extended/json";
import { PublicKeyCredentialWithClientExtensionResults } from "../src/basic/json";
import { credentialCreationOptionsExtended } from "../src/extended/schema";
import {
createExtendedResponseToJSON,
getExtendedResponseToJSON,
} from "../src/extended/api";
import { convert } from "../src/convert";
import "./arraybuffer";

Expand Down Expand Up @@ -75,4 +80,82 @@ describe("extended schema", () => {
]),
);
});

test("converts PublicKeyCredentialWithClientExtensionResults with attestation", () => {
const pkcwa: PublicKeyCredentialWithClientExtensionResults = {
type: "public-key",
id:
"URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENT",
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
clientDataJSON: new Uint8Array([9, 10, 11, 12]),
attestationObject: new Uint8Array([13, 14, 15, 16]),
getTransports: () => ["usb"],
} as AuthenticatorAttestationResponse,
getClientExtensionResults: () =>
({
appidExclude: true,
largeBlob: {
supported: true,
},
} as AuthenticationExtensionsClientOutputs),
};
const converted = createExtendedResponseToJSON(pkcwa);
expect(converted).toEqual({
type: "public-key",
id:
"URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENT",
rawId: "AQIDBA",
response: {
attestationObject: "DQ4PEA",
clientDataJSON: "CQoLDA",
transports: ["usb"],
},
clientExtensionResults: {
appidExclude: true,
largeBlob: {
supported: true,
},
},
});
});

test("converts PublicKeyCredentialWithClientExtensionResults with assertion", () => {
const pkcwa: PublicKeyCredentialWithClientExtensionResults = {
type: "public-key",
id:
"URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENT",
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
authenticatorData: new Uint8Array([5, 6, 7, 8]),
clientDataJSON: new Uint8Array([9, 10, 11, 12]),
signature: new Uint8Array([13, 14, 15, 16]),
userHandle: null,
} as AuthenticatorAssertionResponse,
getClientExtensionResults: () =>
({
largeBlob: {
written: true,
},
} as AuthenticationExtensionsClientOutputs),
};
const converted = getExtendedResponseToJSON(pkcwa);
expect(converted).toEqual({
type: "public-key",
id:
"URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENT",
rawId: "AQIDBA",
response: {
authenticatorData: "BQYHCA",
clientDataJSON: "CQoLDA",
signature: "DQ4PEA",
userHandle: null,
},
clientExtensionResults: {
largeBlob: {
written: true,
},
},
});
});
});
51 changes: 47 additions & 4 deletions test/webauthn-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,57 @@ describe("webauthn schema", () => {
response: {
clientDataJSON: new Uint8Array([9, 10, 11, 12]),
attestationObject: new Uint8Array([13, 14, 15, 16]),
getTransports: () => ["usb"],
} as AuthenticatorAttestationResponse,
getClientExtensionResults: () => ({}),
getClientExtensionResults: () =>
({
appidExclude: true,
credProps: {
rk: true,
},
} as AuthenticationExtensionsClientOutputs),
};
const converted = convert(
bufferToBase64url,
publicKeyCredentialWithAttestation,
pkcwa,
);
expect(converted).toEqual({
type: "public-key",
id:
"URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENT",
rawId: "AQIDBA",
response: {
attestationObject: "DQ4PEA",
clientDataJSON: "CQoLDA",
transports: ["usb"],
},
clientExtensionResults: {
appidExclude: true,
credProps: {
rk: true,
},
},
});
});

test("converts PublicKeyCredentialWithAttestationJSON in browsers without getTransports()", () => {
const pkcwa: PublicKeyCredentialWithClientExtensionResults = {
type: "public-key",
id:
"URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENTIAL_ID-URL_SAFE_BASE_64_CREDENT",
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
clientDataJSON: new Uint8Array([9, 10, 11, 12]),
attestationObject: new Uint8Array([13, 14, 15, 16]),
} as AuthenticatorAttestationResponse,
getClientExtensionResults: () =>
({
appidExclude: true,
credProps: {
rk: true,
},
} as AuthenticationExtensionsClientOutputs),
};
const converted = convert(
bufferToBase64url,
Expand All @@ -115,6 +158,7 @@ describe("webauthn schema", () => {
response: {
attestationObject: "DQ4PEA",
clientDataJSON: "CQoLDA",
transports: [],
},
clientExtensionResults: {
appidExclude: true,
Expand Down Expand Up @@ -232,10 +276,9 @@ describe("webauthn schema", () => {
signature: new Uint8Array([13, 14, 15, 16]),
userHandle: null,
} as AuthenticatorAssertionResponse,
getClientExtensionResults: () => ({}),
clientExtensionResults: {
getClientExtensionResults: () => ({
appid: true,
},
}),
};
const converted = convert(
bufferToBase64url,
Expand Down