rn-passkeys
Version:
A library for using (webauthn) passkeys on iOS, Android & web with a single api
171 lines (154 loc) • 5.53 kB
text/typescript
import type {
AuthenticationCredential,
AuthenticationExtensionsClientInputs,
AuthenticationExtensionsClientOutputs,
AuthenticationExtensionsClientOutputsJSON,
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationCredential,
RegistrationResponseJSON,
} from "./ReactNativePasskeys.types";
import { NotSupportedError } from "./errors";
import { base64URLStringToBuffer, bufferToBase64URLString } from "./utils/base64";
export default {
get name(): string {
return "ReactNativePasskeys";
},
isAutoFillAvalilable(): Promise<boolean> {
const globalPublicKeyCredential = window.PublicKeyCredential;
if (globalPublicKeyCredential.isConditionalMediationAvailable === undefined)
return new Promise((resolve) => resolve(false));
return globalPublicKeyCredential.isConditionalMediationAvailable();
},
isSupported() {
return (
window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function"
);
},
async create({
signal,
...request
}: PublicKeyCredentialCreationOptionsJSON &
Pick<CredentialCreationOptions, "signal">): Promise<RegistrationResponseJSON | null> {
if (!this.isSupported) throw new NotSupportedError();
const credential = (await navigator.credentials.create({
signal,
publicKey: {
...request,
challenge: base64URLStringToBuffer(request.challenge),
user: { ...request.user, id: base64URLStringToBuffer(request.user.id) },
excludeCredentials: request.excludeCredentials?.map((credential) => ({
...credential,
id: base64URLStringToBuffer(credential.id),
// TODO: remove the override when typescript has updated webauthn types
transports: (credential.transports ?? undefined) as AuthenticatorTransport[] | undefined,
})),
},
})) as RegistrationCredential;
// TODO: remove the override when typescript has updated webauthn types
const { largeBlob, ...clientExtensionResults } =
credential?.getClientExtensionResults() as AuthenticationExtensionsClientOutputs;
warnUserOfMissingWebauthnExtensions(request.extensions, clientExtensionResults);
if (!credential) return null;
return {
id: credential?.id,
rawId: credential.id,
response: {
clientDataJSON: bufferToBase64URLString(credential.response.clientDataJSON),
attestationObject: bufferToBase64URLString(credential.response.attestationObject),
},
authenticatorAttachment: undefined,
type: "public-key",
clientExtensionResults: {
...clientExtensionResults,
...(largeBlob && {
largeBlob: {
...largeBlob,
blob: largeBlob?.blob ? bufferToBase64URLString(largeBlob.blob) : undefined,
},
}),
} satisfies AuthenticationExtensionsClientOutputsJSON,
};
},
async get({
mediation,
signal,
...request
}: PublicKeyCredentialRequestOptionsJSON &
Pick<
CredentialRequestOptions,
"mediation" | "signal"
>): Promise<AuthenticationResponseJSON | null> {
const credential = (await navigator.credentials.get({
mediation,
signal,
publicKey: {
...request,
extensions: {
...request.extensions,
largeBlob: {
...request.extensions?.largeBlob,
...(request.extensions?.largeBlob?.write && {
write: base64URLStringToBuffer(request.extensions.largeBlob.write),
}),
},
},
challenge: base64URLStringToBuffer(request.challenge),
allowCredentials: request.allowCredentials?.map((credential) => ({
...credential,
id: base64URLStringToBuffer(credential.id),
// TODO: remove the override when typescript has updated webauthn types
transports: (credential.transports ?? undefined) as AuthenticatorTransport[] | undefined,
})),
},
})) as AuthenticationCredential;
// TODO: remove the override when typescript has updated webauthn types
const extensions =
credential?.getClientExtensionResults() as AuthenticationExtensionsClientOutputs;
warnUserOfMissingWebauthnExtensions(request.extensions, extensions);
const { largeBlob, ...clientExtensionResults } = extensions;
if (!credential) return null;
return {
id: credential.id,
rawId: credential.id,
response: {
clientDataJSON: bufferToBase64URLString(credential.response.clientDataJSON),
authenticatorData: bufferToBase64URLString(credential.response.authenticatorData),
signature: bufferToBase64URLString(credential.response.signature),
userHandle: credential.response.userHandle
? bufferToBase64URLString(credential.response.userHandle)
: undefined,
},
authenticatorAttachment: undefined,
clientExtensionResults: {
...clientExtensionResults,
...(largeBlob && {
largeBlob: {
...largeBlob,
blob: largeBlob?.blob ? bufferToBase64URLString(largeBlob.blob) : undefined,
},
}),
} satisfies AuthenticationExtensionsClientOutputsJSON,
type: "public-key",
};
},
};
/**
* warn the user about extensions that they tried to use that are not supported
*/
const warnUserOfMissingWebauthnExtensions = (
requestedExtensions: AuthenticationExtensionsClientInputs | undefined,
clientExtensionResults: AuthenticationExtensionsClientOutputs | undefined,
) => {
if (clientExtensionResults) {
for (const key in requestedExtensions) {
console.log(key, clientExtensionResults[key]);
if (typeof clientExtensionResults[key] === "undefined") {
alert(
`Webauthn extension ${key} is undefined -- your browser probably doesn't know about it`,
);
}
}
}
};