I am making a sample-app to test a working flow of fido2-server.
I face a ploblem that after registration completed(with Mac touchId or Yubikey 5 series) chrome sais there is no pass key that registered on the website.
Steps I already made were
I need to determine what is the cause of the problem. First I use two way of registration that platform(touch id) and cross-platform(yubikey) for authenticatorAttachment.
when touch id, it sais there is no key to the website(localhost) when yubikey, this security key is not registered to the website.
I think I made misstake of challenge or allowCredential to pass navigator.credentials.get
codes are too long so I put a part of important.
registerRequest
const fido2 = new Fido2Lib({
timeout: 60000,
rpId: "localhost",
rpName: "localhost:3000",
rpIcon: "https://example.com/icon.png",
challengeSize: 32,
attestation: "direct",
authenticatorAttachment: "cross-platform",
authenticatorRequireResidentKey: false,
authenticatorUserVerification: "preferred"
});
const user = users[userId] || {
id: userId,
name: username,
displayName: username,
};
users[userId] = user;
const options = await fido2.attestationOptions();
options.user = user;
options.challenge = base64url.encode(options.challenge);
if (options.user && options.user.id) {
options.user.id = base64url.encode(Buffer.from(options.user.id, 'utf-8'));
}
registerResponse
req.body.rawId = new Uint8Array(base64url.toBuffer(req.body.id)).buffer;
const attestationResult = await fido2.attestationResult(req.body, {
challenge: user.challenge,
origin: "http://localhost:3000",
factor: "either"
});
if (attestationResult.audit.complete) {
const d = attestationResult.authnrData;
user.authenticator = JSON.stringify({
publickey : d.get('credentialPublicKeyPem'),
counter : d.get('counter'),
fmt: d.get('fmt'),
credId : base64url.encode(d.get('credId')),
});
users[userId] = user;
loginResponse
const user = users[userId];
const authenticator = JSON.parse(user.authenticator);
const assertionOptions = await fido2.assertionOptions();
assertionOptions.challenge = base64url.encode(assertionOptions.challenge);
let allowCredentials = [];
allowCredentials.push({
type: 'public-key',
id: authenticator.credId,
transports: ['usb', 'nfc', 'ble']
// transports: ['internal']
});
assertionOptions.allowCredentials = allowCredentials;
client.js for register
const options = response.data;
options.challenge = Uint8Array.from(atob(options.challenge), (c) => c.charCodeAt(0));
options.user.id = Uint8Array.from(atob(options.user.id), (c) => c.charCodeAt(0));
const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;
const res = publicKeyCredentialToJSON(credential.response);
// publicKeyCredentialToJSON is kind of base64encode
if (credential) {
const attestationResponse = {
id: credential.id,
rawId: Array.from(new Uint8Array(credential.rawId)),
type: credential.type,
response: res
};
client.js for login
const options = response.data;
options.challenge = Uint8Array.from(base64url.decode(options.challenge)).buffer;
options.allowCredentials = options.allowCredentials.map((c: any) => {
c.id = Uint8Array.from(base64url.decode(c.id)).buffer;
return c;
});
const assertion = await navigator.credentials.get({ publicKey: options }) as PublicKeyCredential;
The passkey of localhost is successfully saved on my Mac.
Let me know if there is any information missing to help me solve the problem. Or tell me where I'm going wrong in this.
Mostly likely the credential ID is incorrect, or double-encoded.
In a Chromium-based browser, you should be able to open <browser-name>://device-log (e.g. chrome://device-log
) and see the WebAuthn requests logged as JSON structures. Copy-paste those here and check that the credential ID in the get
request looks sensible. Try base64url decoding it and check that the result is not another base64url string, as that suggests double-encoding.